l
В этой главе речь пойдет о процессах. Скомпилированная программа хранится на диске как обычный нетекстовый файл. Когда она будет загружена в память компьютера и начнет выполняться - она станет процессом.
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). Всего программа может одновременно открыть до 20 файлов (считая стандартные каналы), а в некоторых системах и больше (например, 64). В MS DOS есть еще 2 предопределенных канала вывода: stdaux - в последовательный коммуникационный порт, stdprn - на принтер.
d) Процесс имеет уникальный номер, который он может узнать вызовом
int pid = getpid();
а также узнать номер "родителя" вызовом
int ppid = getppid();
Процессы могут по этому номеру посылать друг другу сигналы:
kill(pid /* кому */, sig /* номер сигнала */);
и реагировать на них
signal (sig /*по сигналу*/, f /*вызывать f(sig)*/);
e) Существуют и другие средства коммуникации процессов: семафоры, сообщения, общая память, сетевые коммуникации.
f) Существуют некоторые другие параметры (контекст) процесса: например, его текущий каталог, который достается в наследство от процесса-"родителя", и может быть затем изменен системным вызовом
chdir(char *имя_нового_каталога);
У каждого процесса есть свой собственный текущий рабочий каталог (в отличие от MS DOS, где текущий каталог одинаков для всех задач). К "прочим" характеристикам отнесем также: управляющий терминал; группу процессов (pgrp); идентификатор (номер) владельца процесса (uid), идентификатор группы владельца (gid), реакции и маски, заданные на различные сигналы; и.т.п.
g) Издания других запросов (системных вызовов) к операционной системе ("богу") для выполнения различных "внешних" операций.
h) Все остальные действия происходят внутри процесса и никак не влияют на другие процессы и устройства ("миры"). В частности, один процесс НИКАК не может получить доступ к памяти другого процесса, если тот не позволил ему это явно (механизм shared memory); адресные пространства процессов независимы и изолированы (равно и пространство ядра изолировано от памяти процессов).
Операционная система выступает в качестве коммуникационной среды, связывающей "миры"-процессы, "миры"-внешние устройства (включая терминал пользователя); а также в качестве распорядителя ресурсов "Вселенной", в частности - времени (по очереди выделяемого активным процессам) и пространства (в памяти компьютера и на дисках).
Мы уже неоднократно упоминали "системные вызовы". Что же это такое? С точки зрения Си-программиста - это обычные функции. В них передают аргументы, они возвращают значения. Внешне они ничем не отличаются от написанных нами или библиотечных функций и вызываются из программ одинаковым с ними способом.
С точки же зрения реализации - есть глубокое различие. Тело функции-сисвызова расположено не в нашей программе, а в резидентной (т.е. постоянно находящейся в памяти компьютера) управляющей программе, называемой ядром операционной системы*.
Поведение всех программ в системе вытекает из поведения системных вызовов, которыми они пользуются. Даже то, что UNIX является многозадачной системой, непосредственно вытекает из наличия системных вызовов fork, exec, wait и спецификации их функционирования! То же можно сказать про язык Си - мобильность программы зависит в основном от набора используемых в ней библиотечных функций (и, в меньшей степени, от диалекта самого языка, который должен удовлетворять стандарту на язык Си). Если две разные системы предоставляют все эти функции (которые могут быть по-разному реализованы, но должны делать одно и то же), то программа будет компилироваться и работать в обоих системах, более того, работать в них одинаково.
Сам термин "системный вызов" как раз означает "вызов системы для выполнения действия", т.е. вызов функции в ядре системы. Ядро работает в привелегированном режиме, в котором имеет доступ к некоторым системным таблицам*, регистрам и портам внешних устройств и диспетчера памяти, к которым обычным программам доступ аппаратно запрещен (в отличие от MS DOS, где все таблицы ядра доступны пользовательским программам, что создает раздолье для вирусов). Системный вызов происходит в 2 этапа: сначала в пользовательской программе вызывается библиотечная функция-"корешок", тело которой написано на ассемблере и содержит команду генерации программного прерывания. Это - главное отличие от нормальных Си-функций - вызов по прерыванию. Вторым этапом является реакция ядра на прерывание:
Во время системного вызова (шаг 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().
6.1.1. Используя системный вызов stat, напишите программу, определяющую тип файла: обычный файл, каталог, устройство, FIFO-файл. Ответ:
#include <sys/types.h>
#include <sys/stat.h>
typeOf( name ) char *name;
{ int type; struct stat st;
if( stat( name, &st ) < 0 ){
printf( "%s не существует\n", name );
return 0;
}
printf("Файл имеет %d имен\n", st.st_nlink);
switch(type = (st.st_mode & S_IFMT)){
case S_IFREG:
printf( "Обычный файл размером %ld байт\n",
st.st_size ); break;
case S_IFDIR:
printf( "Каталог\n" ); break;
case S_IFCHR: /* байтоориентированное */
case S_IFBLK: /* блочноориентированное */
printf( "Устройство\n" ); break;
case S_IFIFO:
printf( "FIFO-файл\n" ); break;
default:
printf( "Другой тип\n" ); break;
} return type;
}
6.1.2. Напишите программу, печатающую: свои аргументы, переменные окружения, информацию о всех открытых ею файлах и используемых трубах. Для этой цели используйте системный вызов
struct stat st; int used, fd;
for(fd=0; fd < NOFILE; fd++ ){
used = fstat(fd, &st) < 0 ? 0 : 1;
...
}
Программа может использовать дескрипторы файлов с номерами 0..NOFILE-1 (обычно 0..19). Если fstat для какого-то fd вернул код ошибки (<0), это означает, что данный дескриптор не связан с открытым файлом (т.е. не используется). NOFILE определено в include-файле <sys/param.h>, содержащем разнообразные параметры данной системы.
6.1.3. Напишите упрощенный аналог команды ls, распечатывающий содержимое текущего каталога (файла с именем ".") без сортировки имен по алфавиту. Предусмотрите чтение каталога, чье имя задается как аргумент программы. Имена "." и ".." не выдавать.
Формат каталога описан в header-файле <sys/dir.h> и в "канонической" версии выглядит так: каталог - это файл, состоящий из структур direct, каждая описывает одно имя файла, входящего в каталог:
struct direct {
unsigned short d_ino; /* 2 байта: номер I-узла */
char d_name[DIRSIZ]; /* имя файла */
};
В семействе BSD формат каталога несколько иной - там записи имеют разную длину, зависящую от длины имени файла, которое может иметь длину от 1 до 256 символов.
Имя файла может состоять из любых символов, кроме '\0', служащего признаком конца имени и '/', служащего разделителем. В имени допустимы пробелы, управляющие символы (но не рекомендуются!), любое число точек (в отличие от MS DOS, где допустима единственная точка, отделяющая собственно имя от суффикса (расширения)), разрешены даже непечатные (т.е. управляющие) символы! Если имя файла имеет длину 14 (DIRSIZ) символов, то оно не оканчивается байтом '\0'. В этом случае для печати имени файла возможны три подхода:
char buf[ DIRSIZ + 1 ];
strncpy(buf, d.d_name, DIRSIZ);
buf[ DIRSIZ ] = '\0';
Этот способ лучший, если имя файла надо не просто напечатать, но и запомнить на будущее, чтобы использовать в своей программе.
#include <sys/types.h>
#include <sys/dir.h>
struct direct d;
...
printf( "%*.*s\n", DIRSIZ, DIRSIZ, d.d_name );
Если файл был стерт, то в поле d_ino записи каталога будет содержаться 0 (именно поэтому I-узлы нумеруются начиная с 1, а не с 0). При удалении файла содержимое его (блоки) уничтожается, I-узел освобождается, но имя в каталоге не затирается физически, а просто помечается как стертое: d_ino=0; Каталог при этом никак не уплотняется и не укорачивается! Поэтому имена с d_ino==0 выдавать не следует - это имена уже уничтоженных файлов.
При создании нового имени (creat, link, mknod) система просматривает каталог и переиспользует первый от начала свободный слот (ячейку каталога) где d_ino==0, записывая новое имя в него (только в этот момент старое имя-призрак окончательно исчезнет физически). Если пустых мест нет - каталог удлиняется.
Любой каталог всегда содержит два стандартных имени: "." - ссылка на этот же каталог (на его собственный I-node), ".." - на вышележащий каталог. У корневого каталога "/" оба этих имени ссылаются на него же самого (т.е. содержат d_ino==2).
Имя каталога не содержится в нем самом. Оно содержится в "родительском" каталоге ...
Каталог в UNIX - это обычный дисковый файл. Вы можете читать его из своих программ. Однако никто (включая суперпользователя*) не может записывать что-либо в каталог при помощи write. Изменения содержимого каталогов выполняет только ядро, отвечая на запросы в виде системных вызовов creat, unlink, link, mkdir, rmdir, rename, mknod. Коды доступа для каталога интерпретируются следующим образом:
Совет: для каталога полезно иметь такие коды доступа:
chmod o-w,+t каталог
В системах BSD используется, как уже было упомянуто, формат каталога с переменной длиной записей. Чтобы иметь удобный доступ к именам в каталоге, возникли специальные функции чтения каталога: opendir, closedir, readdir. Покажем, как простейшая команда ls реализуется через эти функции.
#include <stdio.h>
#include <sys/types.h>
#include <dirent.h>
int listdir(char *dirname){
register struct dirent *dirbuf;
DIR *fddir;
ino_t dot_ino = 0, dotdot_ino = 0;
if((fddir = opendir (dirname)) == NULL){
fprintf(stderr, "Can't read %s\n", dirname);
return 1;
}
/* Без сортировки по алфавиту */
while ((dirbuf = readdir (fddir)) != NULL ) {
if (dirbuf->d_ino == 0) continue;
if (strcmp (dirbuf->d_name, "." ) == 0){
dot_ino = dirbuf->d_ino;
continue;
} else if(strcmp (dirbuf->d_name, "..") == 0){
dotdot_ino = dirbuf->d_ino;
continue;
} else printf("%s\n", dirbuf->d_name);
}
closedir (fddir);
if(dot_ino == 0) printf("Поврежденный каталог: нет имени \".\"\n");
if(dotdot_ino == 0) printf("Поврежденный каталог: нет имени \"..\"\n");
if(dot_ino && dot_ino == dotdot_ino) printf("Это корневой каталог диска\n");
return 0;
}
int main(int ac, char *av[]){
int i;
if(ac > 1) for(i=1; i < ac; i++) listdir(av[i]);
else listdir(".");
return 0;
}
Обратите внимание, что тут не требуется добавление '\0' в конец поля d_name, поскольку его предоставляет нам сама функция readdir().
6.1.4. Напишите программу удаления файлов и каталогов, заданных в argv. Делайте stat, чтобы определить тип файла (файл/каталог). Программа должна отказываться удалять файлы устройств. Для удаления пустого каталога (не содержащего иных имен, кроме "." и "..") следует использовать сисвызов
rmdir(имя_каталога);
(если каталог не пуст - errno получит значение EEXIST); а для удаления обычных файлов (не каталогов)
unlink(имя_файла);
Программа должна запрашивать подтверждение на удаление каждого файла, выдавая его имя, тип, размер в килобайтах и вопрос "удалить ?".
* - Именно это имя показывает команда ps -ef
* - Собственно, операционная система характеризуется набором предоставляемых ею системных вызовов, поскольку все концепции, заложенные в системе, доступны нам только через них. Если мы имеем две реализации системы с разным внутренним устройством ядер, но предоставляющие одинаковый интерфейс системных вызовов (их набор, смысл и поведение), то это все-таки одна и та же система! Ядра могут не просто отличаться, но и быть построенными на совершенно различных принципах: так обстоит дело с UNIX-ами на однопроцессорных и многопроцессорных машинах. Но для нас ядро - это "черный ящик", полностью определяемый его поведением, т.е. своим интерфейсом с программами, но не внутренним устройством. Вторым параметром, характеризующим ОС, являются форматы данных, используемые системой: форматы данных для сисвызовов и формат информации в различных файлах, в том числе формат оформления выполняемых файлов (формат данных в физической памяти машины в этот список не входит - он зависим от реализации и от процессора). Как правило, программа пишется так, чтобы использовать соглашения, принятые в данной системе, для чего она просто включает ряд стандартных include-файлов с описанием этих форматов. Имена этих файлов также можно отнести к интерфейсу системы.
* - Таким как таблица процессов, таблица открытых файлов (всех вместе и для каждого процесса), и.т.п.
* - Суперпользователь (superuser) имеет uid==0. Это "привелегированный" пользователь, который имеет право делать ВСЕ. Ему доступны любые сисвызовы и файлы, несмотря на коды доступа и.т.п.
6.1.5. Напишите функцию рекурсивного обхода дерева подкаталогов и печати имен всех файлов в нем. Ключ U42 означает файловую систему с длинными именами файлов (BSD 4.2).
/*#!/bin/cc -DFIND -DU42 -DMATCHONLY treemk.c match.c -o tree -lx
* Обход поддерева каталогов (по мотивам Керниган & Ритчи).
* Ключи компиляции:
* BSD-4.2 BSD-4.3 -DU42
* XENIX с канонической файл.сист. ничего
* XENIX с библиотекой -lx -DU42
* программа поиска файлов -DFIND
* программа рекурсивного удаления -DRM_REC
* программа подсчета используемого места на диске БЕЗ_КЛЮЧА
*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/param.h> /* для MAXPATHLEN */
#if defined(M_XENIX) && defined(U42)
# include <sys/ndir.h> /* XENIX + U42 эмуляция */
#else
# include <dirent.h>
# define stat(f,s) lstat(f,s) /* не проходить по символьным ссылкам */
# define d_namlen d_reclen
#endif
/* проверка: каталог ли это */
#define isdir(st) ((st.st_mode & S_IFMT) == S_IFDIR)
struct stat st; /* для сисвызова stat() */
char buf[MAXPATHLEN+1]; /* буфер для имени файла */
#define FAILURE (-1) /* код неудачи */
#define SUCCESS 1 /* код успеха */
#define WARNING 0 /* нефатальная ошибка */
/* Сообщения об ошибках во время обхода дерева: */
#ifndef ERR_CANT_READ
# define ERR_CANT_READ(name) \
fprintf( stderr, "\tНе могу читать \"%s\"\n", name), WARNING
# define ERR_NAME_TOO_LONG() \
fprintf( stderr, "\tСлишком длинное полное имя\n" ), WARNING
#endif
/* Прототипы для предварительного объявления функций. */
extern char *strrchr(char *, char);
int directory (char *name, int level,
int (*enter)(char *full, int level, struct stat *st),
int (*leave)(char *full, int level),
int (*touch)(char *full, int level, struct stat *st));
/* Функции-обработчики enter, leave, touch должны
* возвращать (-1) для прерывания просмотра дерева,
* либо значение >= 0 для продолжения. */
/* Обойти дерево с корнем в rootdir */
int walktree (
char *rootdir, /* корень дерева */
int (*enter)(char *full, int level, struct stat *st),
int (*leave)(char *full, int level),
int (*touch)(char *full, int level, struct stat *st)
){
/* проверка корректности корня */
if( stat(rootdir, &st) < 0 || !isdir(st)){
fprintf( stderr, "\tПлохой корень дерева \"%s\"\n", rootdir );
return FAILURE; /* неудача */
}
strcpy (buf, rootdir);
return act (buf, 0, enter, leave, touch);
}
/* Оценка файла с именем name.
*/
int act (char *name, int level,
int (*enter)(char *full, int level, struct stat *st),
int (*leave)(char *full, int level),
int (*touch)(char *full, int level, struct stat *st))
{
if (stat (name, &st) < 0)
return WARNING; /* ошибка, но не фатальная */
if(isdir(st)){ /* позвать обработчик каталогов */
if(enter)
if( enter(name, level, &st) == FAILURE ) return FAILURE;
return directory (name, level+1, enter, leave, touch);
} else { /* позвать обработчик файлов */
if(touch) return touch (name, level, &st);
else return SUCCESS;
}
}
/* Обработать каталог: прочитать его и найти подкаталоги */
int directory (char *name, int level,
int (*enter)(char *full, int level, struct stat *st),
int (*leave)(char *full, int level),
int (*touch)(char *full, int level, struct stat *st))
{
#ifndef U42
struct direct dirbuf;
int fd;
#else
register struct dirent *dirbuf;
DIR *fd;
extern DIR *opendir();
#endif
char *nbp, *tail, *nep;
int i, retcode = SUCCESS;
#ifndef U42
if ((fd = open (name, 0)) < 0) {
#else
if ((fd = opendir (name)) == NULL) {
#endif
return ERR_CANT_READ(name);
}
tail = nbp = name + strlen (name); /* указатель на закрывающий \0 */
if( strcmp( name, "/" )) /* если не "/" */
*nbp++ = '/';
*nbp = '\0';
#ifndef U42
if (nbp + DIRSIZ + 2 >= name + MAXPATHLEN) {
*tail = '\0';
return ERR_NAME_TOO_LONG();
}
#endif
#ifndef U42
while (read(fd, (char *) &dirbuf, sizeof(dirbuf)) == sizeof(dirbuf)){
if (dirbuf.d_ino == 0) /* стертый файл */
continue;
if (strcmp (dirbuf.d_name, "." ) == 0 ||
strcmp (dirbuf.d_name, "..") == 0) /* не интересуют */
continue;
for (i = 0, nep = nbp; i < DIRSIZ; i++)
*nep++ = dirbuf.d_name[i];
# else /*U42*/
while ((dirbuf = readdir (fd)) != NULL ) {
if (dirbuf->d_ino == 0)
continue;
if (strcmp (dirbuf->d_name, "." ) == 0 ||
strcmp (dirbuf->d_name, "..") == 0)
continue;
for (i = 0, nep = nbp; i < dirbuf->d_namlen ; i++)
*nep++ = dirbuf->d_name[i];
#endif /*U42*/
*nep = '\0';
if( act(name, level, enter, leave, touch) == FAILURE) {
retcode = FAILURE; break; }
}
#ifndef U42
close (fd);
#else
closedir(fd);
#endif
*tail = '\0'; /* восстановить старое name */
if(retcode != FAILURE && leave)
if( leave(name, level) == FAILURE) retcode = FAILURE;
return retcode;
}
/* -------------------------------------------------------------- */
/* Disk Usage -- Оценка места, занимаемого файлами поддерева */
/* -------------------------------------------------------------- */
/* Пересчет байтов в килобайты */
#define KB(s) (((s)/1024L) + ((s)%1024L ? 1L:0L))
/* или #define KB(s) (((s) + 1024L - 1) / 1024L) */
long size; /* общий размер */
long nfiles; /* всего файлов */
long ndirs; /* из них каталогов */
#define WARNING_LIMIT 150L /* подозрительно большой файл */
static int du_touch (char *name, int level, struct stat *st){
long sz;
size += (sz = KB(st->st_size)); /* размер файла в Кб. */
nfiles++;
#ifndef TREEONLY
if( sz >= WARNING_LIMIT )
fprintf(stderr,"\tВнимание! \"%s\" очень большой: %ld Кб.\n",
name, sz);
#endif /*TREEONLY*/
return SUCCESS;
}
static int du_enter (char *name, int level, struct stat *st){
#ifndef TREEONLY
fprintf( stderr, "Каталог \"%s\"\n", name );
#endif
size += KB(st->st_size); /* размер каталога в Кб. */
nfiles++; ++ndirs; return SUCCESS;
}
long du (char *name){
size = nfiles = ndirs = 0L;
walktree(name, du_enter, NULL, du_touch );
return size;
}
/* -------------------------------------------------------------- */
/* Рекурсивное удаление файлов и каталогов */
/* -------------------------------------------------------------- */
int deleted; /* сколько файлов и каталогов удалено */
static int recrm_dir (char *name, int level){
if( rmdir(name) >= 0){ deleted++; return SUCCESS; }
fprintf(stderr, "Не могу rmdir '%s'\n", name); return WARNING;
}
static int recrm_file(char *name, int level, struct stat *st){
if( unlink(name) >= 0){ deleted++; return SUCCESS; }
fprintf(stderr, "Не могу rm '%s'\n", name); return WARNING;
}
int recrmdir(char *name){
int ok_code; deleted = 0;
ok_code = walktree(name, NULL, recrm_dir, recrm_file);
printf("Удалено %d файлов и каталогов в %s\n", deleted, name);
return ok_code;
}
/* -------------------------------------------------------------- */
/* Поиск файлов с подходящим именем (по шаблону имени) */
/* -------------------------------------------------------------- */
char *find_PATTERN;
static int find_check(char *fullname, int level, struct stat *st){
char *basename = strrchr(fullname, '/');
if(basename) basename++;
else basename = fullname;
if( match(basename, find_PATTERN))
printf("Level#%02d %s\n", level, fullname);
if( !strcmp( basename, "core")){
printf("Найден дамп %s, поиск прекращен.\n", fullname);
return FAILURE;
}
return SUCCESS;
}
void find (char *root, char *pattern){
find_PATTERN = pattern;
walktree(root, find_check, NULL, find_check);
}
/* -------------------------------------------------------------- */
#ifndef TREEONLY
void main(int argc, char *argv[]){
#ifdef FIND
if(argc != 3){ fprintf(stderr, "Arg count\n"); exit(1); }
find(argv[1], argv[2]);
#else
# ifdef RM_REC
for(argv++; *argv; argv++)
recrmdir(*argv);
# else
du( argc == 1 ? "." : argv[1] );
printf( "%ld килобайт в %ld файлах.\n", size, nfiles );
printf( "%ld каталогов.\n", ndirs );
# endif
#endif
exit(0);
}
#endif /*TREEONLY*/
6.1.6. Используя предыдущий алгоритм, напишите программу рекурсивного копирования поддерева каталогов в другое место. Для создания новых каталогов используйте системный вызов
mkdir(имя_каталога, коды_доступа);
6.1.7. Используя тот же алгоритм, напишите программу удаления каталога, которая удаляет все файлы в нем и, рекурсивно, все его подкаталоги. Таким образом, удаляется дерево каталогов. В UNIX подобную операцию выполняет команда
rm -r имя_каталога_корня_дерева
6.1.8. Используя все тот же алгоритм обхода, напишите аналог команды find, который будет позволять:
(st.st_mode & 0111) != 0
Как уже ясно, следует пользоваться вызовом stat для проверки каждого файла.
6.2.1. Напишите функцию, переводящую год, месяц, день, часы, минуты и секунды в число секунд, прошедшее до указанного момента с 00 часов 00 минут 00 секунд 1 Января 1970 года. Внимание: результат должен иметь тип long (точнее time_t).
Эта функция облегчит вам сравнение двух моментов времени, заданных в общепринятом "человеческом" формате, поскольку сравнить два long числа гораздо проще, чем сравнивать по очереди годы, затем, если они равны - месяцы, если месяцы равны - даты, и.т.д.; а также облегчит измерение интервала между двумя событиями - он вычисляется просто как разность двух чисел. В системе 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 ] );
Пример использования этих функций есть в приложении. Установить время в системе может суперпользователь вызовом
stime(&t);
6.2.2. Напишите функцию печати текущего времени в формате ЧЧ:ММ:СС ДД-МЕС-ГГ. Используйте системный вызов time() и функцию localtime().
Существует стандартная функция ctime(), которая печатает время в формате:
/* Mon Mar 25 18:56:36 1991 */
#include <stdio.h>
#include <time.h>
main(){ /* команда date */
time_t t = time(NULL);
char *s = ctime(&t);
printf("%s", s);
}
Обратите внимание, что строка s уже содержит на конце символ '\n'.
6.2.3. Структура stat, заполняемая системным вызовом stat(), кроме прочих полей содержит поля типа time_t st_ctime, st_mtime и st_atime - время последнего изменения содержимого I-узла файла, время последнего изменения файла и время последнего доступа к файлу.
Модифицируйте функцию typeOf(), чтобы она печатала еще и эти даты.
utime(имяФайла, NULL);
Он используется для взаимодействия с программой make - в команде touch. Изменить время можно только своему файлу.
6.2.4. Напишите аналог команды ls -tm, выдающей список имен файлов текущего каталога, отсортированный по убыванию поля st_mtime, то есть недавно модифицированные файлы выдаются первыми. Для каждого прочитанного из каталога имени надо сделать stat; имена файлов и времена следует сохранить в массиве структур, а затем отсортировать его.
6.2.5. Напишите аналогичную программу, сортирующую файлы в порядке возрастания их размера (st_size).
6.2.6. Напишите аналог команды ls -l, выдающий имена файлов каталога и их коды доступа в формате rwxrw-r--. Для получения кодов доступа используйте вызов stat
stat( имяФайла, &st);
кодыДоступа = st.st_mode & 0777;
Для изменения кодов доступа используется вызов
chmod(имя_файла, новые_коды);
Можно изменять коды доступа, соответствующие битовой маске
0777 | S_ISUID | S_ISGID | S_ISVTX
(смотри <sys/stat.h>). Тип файла (см. функцию typeOf) не может быть изменен. Изменить коды доступа к файлу может только его владелец.
Печатайте еще номер I-узла файла: поле d_ino каталога либо поле st_ino структуры stat.
6.2.7. Вот программа, которая каждые 2 секунды проверяет - не изменилось ли содержимое текущего каталога:
#include <sys/types.h>
#include <sys/stat.h>
extern char *ctime();
main(){
time_t last; struct stat st;
for( stat(".", &st), last=st.st_mtime; ; sleep(2)){
stat(".", &st);
if(last != st.st_mtime){
last = st.st_mtime;
printf("Был создан или удален какой-то файл: %s",
ctime(&last));
}
}
}
Модифицируйте ее, чтобы она сообщала какое имя (имена) было удалено или создано (для этого надо при запуске программы прочитать и запомнить содержимое каталога, а при обнаружении модификации - перечитать каталог и сравнить его с прежним содержимым).
6.2.8. Напишите по аналогии программу, которая выдает сообщение, если указанный вами файл был кем-то прочитан, записан или удален. Вам следует отслеживать изменение полей st_atime, st_mtime и значение stat() < 0 соответственно. Если файл удален - программа завершается.
6.2.9. Современные 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;
sighold(SIGALRM);
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);
}
* - Время модификации файла можно изменить на текущее астрономическое время и не производя записи в файл. Для этого используется вызов
6.2.10. Второй пример использования таймера - это таймер, отсчитывающий текущее время суток (а также дату). Чтобы получить значение этого таймера используется вызов функции gettimeofday
#include <time.h>
void main(){
struct timeval timenow;
gettimeofday(&timenow, NULL);
printf("%u sec, %u msec\n",
timenow.tv_sec,
timenow.tv_usec
);
printf("%s", ctime(&timenow.tv_sec));
exit(0);
}
Поле tv_sec содержит число секунд, прошедшее с полуночи 1 января 1970 года до данного момента; в чем полностью соответствует системному вызову time. Однако плюс к тому поле tv_usec содержит число миллионных долей текущей секунды (значение этого поля всегда меньше 1000000).
6.2.11. К данному параграфу вернитесь, изучив раздел про fork() и exit(). Каждый процесс может пребывать в двух фазах: системной (внутри тела системного вызова - его выполняет для нас ядро операционной системы) и пользовательской (внутри кода самой программы). Время, затраченное процессом в каждой фазе, может быть измеряно системным вызовом times(). Кроме того, этот вызов позволяет узнать суммарное время, затраченное порожденными процессами (порожденными при помощи fork). Системный вызов заполняет структуру
struct tms {
clock_t tms_utime;
clock_t tms_stime;
clock_t tms_cutime;
clock_t tms_cstime;
};
и возвращает значение
#include <sys/times.h>
struct tms time_buf;
clock_t real_time = times(&time_buf);
Все времена измеряются в "тиках" - некоторых долях секунды. Число тиков в секунде можно узнать таким системным вызовом (в системе Solaris):
#include <unistd.h>
clock_t HZ = sysconf(_SC_CLK_TCK);
В старых системах, где таймер работал от сети переменного тока, это число получалось равным 60 (60 Герц - частота сети переменного тока). В современных системах это 100. Поля структуры содержат:
tms_utime
время, затраченное вызывающим процессом в пользовательской фазе.
tms_stime
время, затраченное вызывающим процессом в системной фазе.
tms_cutime
время, затраченное порожденными процессами в пользовательской фазе: оно равно сумме всех tms_utime и tms_cutime порожденных процессов (рекурсивное суммирование).
tms_cstime
время, затраченное порожденными процессами в системной фазе: оно равно сумме всех tms_stime и tms_cstime порожденных процессов (рекурсивное суммирование).
real_time
время, соответствующее астрономическому времени системы. Имеет смысл мерять только их разность.
Вот пример программы:
#include <stdio.h>
#include <unistd.h> /* _SC_CLK_TCK */
#include <signal.h> /* SIGALRM */
#include <sys/time.h> /* не используется */
#include <sys/times.h> /* struct tms */
struct tms tms_stop, tms_start;
clock_t real_stop, real_start;
clock_t HZ; /* число ticks в секунде */
/* Засечь время момента старта процесса */
void hello(void){
real_start = times(&tms_start);
}
/* Засечь время окончания процесса */
void bye(int n){
real_stop = times(&tms_stop);
#ifdef CRONO
/* Разность времен */
tms_stop.tms_utime -= tms_start.tms_utime;
tms_stop.tms_stime -= tms_start.tms_stime;
#endif
/* Распечатать времена */
printf("User time = %g seconds [%lu ticks]\n",
tms_stop.tms_utime / (double)HZ, tms_stop.tms_utime);
printf("System time = %g seconds [%lu ticks]\n",
tms_stop.tms_stime / (double)HZ, tms_stop.tms_stime);
printf("Children user time = %g seconds [%lu ticks]\n",
tms_stop.tms_cutime / (double)HZ, tms_stop.tms_cutime);
printf("Children system time = %g seconds [%lu ticks]\n",
tms_stop.tms_cstime / (double)HZ, tms_stop.tms_cstime);
printf("Real time = %g seconds [%lu ticks]\n",
(real_stop - real_start) / (double)HZ, real_stop - real_start);
exit(n);
}
/* По сигналу SIGALRM - завершить процесс */
void onalarm(int nsig){
printf("Выход #%d ================\n", getpid());
bye(0);
}
/* Порожденный процесс */
void dochild(int n){
hello();
printf("Старт #%d ================\n", getpid());
signal(SIGALRM, onalarm);
/* Заказать сигнал SIGALRM через 1 + n*3 секунд */
alarm(1 + n*3);
for(;;){} /* зациклиться в user mode */
}
#define NCHLD 4
int main(int ac, char *av[]){
int i;
/* Узнать число тиков в секунде */
HZ = sysconf(_SC_CLK_TCK);
setbuf(stdout, NULL);
hello();
for(i=0; i < NCHLD; i++)
if(fork() == 0)
dochild(i);
while(wait(NULL) > 0);
printf("Выход MAIN =================\n");
bye(0);
return 0;
}
и ее выдача:
Старт #3883 ================
Старт #3884 ================
Старт #3885 ================
Старт #3886 ================
Выход #3883 ================
User time = 0.72 seconds [72 ticks]
System time = 0.01 seconds [1 ticks]
Children user time = 0 seconds [0 ticks]
Children system time = 0 seconds [0 ticks]
Real time = 1.01 seconds [101 ticks]
Выход #3884 ================
User time = 1.88 seconds [188 ticks]
System time = 0.01 seconds [1 ticks]
Children user time = 0 seconds [0 ticks]
Children system time = 0 seconds [0 ticks]
Real time = 4.09 seconds [409 ticks]
Выход #3885 ================
User time = 4.41 seconds [441 ticks]
System time = 0.01 seconds [1 ticks]
Children user time = 0 seconds [0 ticks]
Children system time = 0 seconds [0 ticks]
Real time = 7.01 seconds [701 ticks]
Выход #3886 ================
User time = 8.9 seconds [890 ticks]
System time = 0 seconds [0 ticks]
Children user time = 0 seconds [0 ticks]
Children system time = 0 seconds [0 ticks]
Real time = 10.01 seconds [1001 ticks]
Выход MAIN =================
User time = 0.01 seconds [1 ticks]
System time = 0.04 seconds [4 ticks]
Children user time = 15.91 seconds [1591 ticks]
Children system time = 0.03 seconds [3 ticks]
Real time = 10.41 seconds [1041 ticks]
Обратите внимание, что 72+188+441+890=1591 (поле tms_cutime для main).
6.2.12. Еще одна программа: хронометрирование выполнения другой программы. Пример:
timer ls -l
/* Хронометрирование выполнения программы */
#include <stdio.h>
#include <unistd.h>
#include <sys/times.h>
extern errno;
typedef struct _timeStamp {
clock_t real_time;
clock_t cpu_time;
clock_t child_time;
clock_t child_sys, child_user;
} TimeStamp;
TimeStamp TIME(){
struct tms tms;
TimeStamp st;
st.real_time = times(&tms);
st.cpu_time = tms.tms_utime +
tms.tms_stime +
tms.tms_cutime +
tms.tms_cstime;
st.child_time = tms.tms_cutime +
tms.tms_cstime;
st.child_sys = tms.tms_cstime;
st.child_user = tms.tms_cutime;
return st;
}
void PRTIME(TimeStamp start, TimeStamp stop){
clock_t HZ = sysconf(_SC_CLK_TCK);
clock_t real_time = stop.real_time - start.real_time;
clock_t cpu_time = stop.cpu_time - start.cpu_time;
clock_t child_time = stop.child_time - start.child_time;
printf("%g real, %g cpu, %g child (%g user, %g sys), %ld%%\n",
real_time / (double)HZ,
cpu_time / (double)HZ,
child_time / (double)HZ,
stop.child_user / (double)HZ,
stop.child_sys / (double)HZ,
(child_time * 100L) / (real_time ? real_time : 1)
);
}
TimeStamp start, stop;
int main(int ac, char *av[]){
char *prog = *av++;
if(*av == NULL){
fprintf(stderr, "Usage: %s command [args...]\n", prog);
return(1);
}
start = TIME();
if(fork() == 0){
execvp(av[0], av);
perror(av[0]);
exit(errno);
}
while(wait(NULL) > 0);
stop = TIME();
PRTIME(start, stop);
return(0);
}
6.3.1. Системный вызов ustat() позволяет узнать количество свободного места в файловой системе, содержащей заданный файл (в примере ниже - текущий каталог):
#include <sys/types.h>
#include <sys/stat.h>
#include <ustat.h>
struct stat st; struct ustat ust;
void main(int ac, char *av[]){
char *file = (ac==1 ? "." : av[1]);
if( stat(file, &st) < 0) exit(1);
ustat(st.st_dev, &ust);
printf("На диске %*.*s\n"
"%ld свободных блоков (%ld Кб)\n"
"%d свободных I-узлов\n",
sizeof ust.f_fname, sizeof ust.f_fname,
ust.f_fname, /* название файловой системы (метка) */
ust.f_tfree, /* блоки по 512 байт */
(ust.f_tfree * 512L) / 1024,
ust.f_tinode );
}
Обратите внимание на запись длинной строки в printf: строки, перечисленные последовательно, склеиваются ANSI C компилятором в одну длинную строку:
char s[] = "This is" " a line " "of words";
совпадает с
char s[] = "This is a line of words";
6.3.2. Более правильно, однако, пользоваться сисвызовом statvfs - статистика по виртуальной файловой системе. Рассмотрим его в следующем примере: копирование файла с проверкой на наличие свободного места.
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <stdarg.h>
#include <fcntl.h> /* O_RDONLY */
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/statvfs.h>
#include <sys/param.h> /* MAXPATHLEN */
char *progname; /* имя программы */
void error(char *fmt, ...){
va_list args;
va_start(args, fmt);
fprintf(stderr, "%s: ", progname);
vfprintf(stderr, fmt, args);
fputc('\n', stderr);
va_end(args);
}
int copyFile(char *to, char *from){ /* куда, откуда */
char newname[MAXPATHLEN+1];
char answer[20];
struct stat stf, stt;
int fdin, fdout;
int n, code = 0;
char iobuf[64 * 1024];
char *dirname = NULL, *s;
if((fdin = open(from, O_RDONLY)) < 0){
error("Cannot read %s", from);
return (-1);
}
fstat(fdin, &stf);
if((stf.st_mode & S_IFMT) == S_IFDIR){
close(fdin);
error("%s is a directory", from);
return (-2);
}
if(stat(to, &stt) >= 0){
/* Файл уже существует */
if((stt.st_mode & S_IFMT) == S_IFDIR){
/* И это каталог */
/* Выделить последнюю компоненту пути from */
if((s = strrchr(from, '/')) && s[1])
s++;
else s = from;
dirname = to;
/* Целевой файл - файл в этом каталоге */
sprintf(newname, "%s/%s", to, s);
to = newname;
if(stat(to, &stt) < 0)
goto not_exist;
}
if(stt.st_dev == stf.st_dev && stt.st_ino == stf.st_ino){
error("%s: cannot copy file to itself", from);
return (-3);
}
switch(stt.st_mode & S_IFMT){
case S_IFBLK:
case S_IFCHR:
case S_IFIFO:
break;
default:
printf("%s already exists, overwrite ? ", to);
fflush(stdout);
*answer = '\0';
gets(answer);
if(*answer != 'y'){ /* NO */
close(fdin);
return (-4);
}
break;
}
}
not_exist:
printf("COPY %s TO %s\n", from, to);
if((stf.st_mode & S_IFMT) == S_IFREG){
/* Проверка наличия свободного места в каталоге dirname */
struct statvfs fs;
char tmpbuf[MAXPATHLEN+1];
if(dirname == NULL){
/* То 'to' - это имя файла, а не каталога */
strcpy(tmpbuf, to);
if(s = strrchr(tmpbuf, '/')){
if(*tmpbuf != '/' || s != tmpbuf){
/* Имена "../xxx"
* и второй случай:
* абсолютные имена не в корне,
* то есть не "/" и не "/xxx"
*/
*s = '\0';
}else{
/* "/" или "/xxx" */
if(s[1]) s[1] = '\0';
}
dirname = tmpbuf;
} else dirname = ".";
}
if(statvfs(dirname, &fs) >= 0){
size_t size = (geteuid() == 0 ) ?
/* Доступно суперпользователю: байт */
fs.f_frsize * fs.f_bfree :
/* Доступно обычному пользователю: байт */
fs.f_frsize * fs.f_bavail;
if(size < stf.st_size){
error("Not enough free space on %s: have %lu, need %lu",
dirname, size, stf.st_size);
close(fdin);
return (-5);
}
}
}
if((fdout = creat(to, stf.st_mode)) < 0){
error("Can't create %s", to);
close(fdin);
return (-6);
} else {
fchmod(fdout, stf.st_mode);
fchown(fdout, stf.st_uid, stf.st_gid);
}
while (n = read (fdin, iobuf, sizeof iobuf)) {
if(n < 0){
error ("read error");
code = (-7);
goto done;
}
if(write (fdout, iobuf, n) != n) {
error ("write error");
code = (-8);
goto done;
}
}
done:
close (fdin);
close (fdout);
/* Проверить: соответствует ли результат ожиданиям */
if(stat(to, &stt) >= 0 && (stt.st_mode & S_IFMT) == S_IFREG){
if(stf.st_size < stt.st_size){
error("File has grown at the time of copying");
} else if(stf.st_size > stt.st_size){
error("File too short, target %s removed", to);
unlink(to);
code = (-9);
}
}
return code;
}
int main(int argc, char *argv[]){
int i, code = 0;
progname = argv[0];
if(argc < 3){
error("Usage: %s from... to", argv[0]);
return 1;
}
for(i=1; i < argc-1; i++)
code |= copyFile(argv[argc-1], argv[i]) < 0 ? 1 : 0;
return code;
}
Возвращаемая структура struct statvfs содержит такие поля (в частности):
Типа long:
f_frsize размер блока
f_blocks размер файловой системы в блоках
f_bfree свободных блоков (для суперпользователя)
f_bavail свободных блоков (для всех остальных)
f_files число I-nodes в файловой системе
f_ffree свободных I-nodes (для суперпользователя)
f_favail свободных I-nodes (для всех остальных)
Типа char *
f_basetype тип файловой системы: ufs, nfs, ...
По два значения дано потому, что операционная система резервирует часть файловой системы для использования ТОЛЬКО суперпользователем (чтобы администратор смог распихать файлы в случае переполнения диска, и имел резерв на это). ufs - это UNIX file system из BSD 4.x
Процессы в UNIX используют много разных механизмов взаимодействия. Одним из них являются сигналы.
Сигналы - это асинхронные события. Что это значит? Сначала объясним, что такое синхронные события: я два раза в день подхожу к почтовому ящику и проверяю - нет ли в нем почты (событий). Во-первых, я произвожу опрос - "нет ли для меня события?", в программе это выглядело бы как вызов функции опроса и, может быть, ожидания события. Во-вторых, я знаю, что почта может ко мне прийти, поскольку я подписался на какие-то газеты. То есть я предварительно заказывал эти события.
Схема с синхронными событиями очень распространена. Кассир сидит у кассы и ожидает, пока к нему в окошечко не заглянет клиент. Поезд периодически проезжает мимо светофора и останавливается, если горит красный. Функция Си пассивно "спит" до тех пор, пока ее не вызовут; однако она всегда готова выполнить свою работу (обслужить клиента). Такое ожидающее заказа (события) действующее лицо называется сервер. После выполнения заказа сервер вновь переходит в состояние ожидания вызова. Итак, если событие ожидается в специальном месте и в определенные моменты времени (издается некий вызов для ОПРОСА) - это синхронные события. Канонический пример - функция gets, которая задержит выполнение программы, пока с клавиатуры не будет введена строка. Большинство ожиданий внутри системных вызовов - синхронны. Ядро ОС выступает для программ пользователей в роли сервера, выполняющего сисвызовы (хотя и не только в этой роли - ядро иногда предпринимает и активные действия: передача процессора другому процессу через определенное время (режим разделения времени), убивание процесса при ошибке, и.т.п.).
Сигналы - это асинхронные события. Они приходят неожиданно, в любой момент времени - вроде телефонного звонка. Кроме того, их не требуется заказывать - сигнал процессу может поступить совсем без повода. Аналогия из жизни такова: человек сидит и пишет письмо. Вдруг его окликают посреди фразы - он отвлекается, отвечает на вопрос, и вновь продолжает прерванное занятие. Человек не ожидал этого оклика (быть может, он готов к нему, но он не озирался по сторонам специально). Кроме того, сигнал мог поступить когда он писал 5-ое предложение, а мог - когда 34-ое. Момент времени, в который произойдет прерывание, не фиксирован.
Сигналы имеют номера, причем их количество ограничено - есть определенный список допустимых сигналов. Номера и мнемонические имена сигналов перечислены в includeфайле <signal.h> и имеют вид SIGнечто. Допустимы сигналы с номерами 1..NSIG-1, где NSIG определено в этом файле. При получении сигнала мы узнаем его номер, но не узнаем никакой иной информации: ни от кого поступил сигнал, ни что от нас хотят. Просто "звонит телефон". Чтобы получить дополнительную информацию, наш процесс должен взять ее из другого известного места; например - прочесть заказ из некоторого файла, об имени которого все наши программы заранее "договорились". Сигналы процессу могут поступать тремя путями:
kill(pid, sig);где pid - идентификатор (номер) процесса-получателя, а sig - номер сигнала. Послать сигнал можно только родственному процессу - запущенному тем же пользователем.
Процесс-получатель должен как-то отреагировать на сигнал. Программа может:
В большинстве случаев сигнал по умолчанию убивает процесс-получатель. Однако процесс может изменить это умолчание и задать свою реакцию явно. Это делается вызовом signal:
#include <signal.h>
void (*signal(int sig, void (*react)() )) ();
Параметр react может иметь значение:
void fr(gotsig){ ..... } /* обработчик */
... signal (sig, fr); ... /* задание реакции */
Тогда при получении сигнала sig будет вызвана функция fr, в которую в качестве
аргумента системой будет передан номер сигнала, действительно вызвавшего ее gotsig==sig. Это полезно, т.к. можно задать одну и ту же функцию в качестве
реакции для нескольких сигналов:
... signal (sig1, fr); signal(sig2, fr); ...
После возврата из функции fr() программа продолжится с прерванного места. Перед
вызовом функции-обработчика реакция автоматически сбрасывается в реакцию по
умолчанию SIG_DFL, а после выхода из обработчика снова восстанавливается в fr.
Это значит, что во время работы функции-обработчика может прийти сигнал, который
убьет программу.
Приведем список некоторых сигналов; полное описание посмотрите в документации. Колонки таблицы: G - может быть перехвачен; D - по умолчанию убивает процесс (k), игнорируется (i); C - образуется дамп памяти процесса: файл core, который затем может быть исследован отладчиком adb; F - реакция на сигнал сбрасывается; S - посылается обычно системой, а не явно.
сигнал G D C F S смысл
SIGTERM + k - + - завершить процесс
SIGKILL - k - + - убить процесс
SIGINT + k - + - прерывание с клавиш
SIGQUIT + k + + - прерывание с клавиш
SIGALRM + k - + + будильник
SIGILL + k + - + запрещенная команда
SIGBUS + k + + + обращение по неверному
SIGSEGV + k + + + адресу
SIGUSR1, USR2 + i - + - пользовательские
SIGCLD + i - + + смерть потомка
Некоторые версии UNIX предоставляют более развитые средства работы с сигналами. Опишем некоторые из средств, имеющихся в BSD (в других системах они могут быть смоделированы другими способами).
Пусть у нас в программе есть "критическая секция", во время выполнения которой приход сигналов нежелателен. Мы можем "заморозить" (заблокировать) сигнал, отложив момент его поступления до "разморозки":
|
sighold(sig); заблокировать сигнал
| :
КРИТИЧЕСКАЯ :<---процессу послан сигнал sig,
СЕКЦИЯ : но он не вызывает реакцию немедленно,
| : а "висит", ожидая разрешения.
| :
sigrelse(sig); разблокировать
|<----------- sig
| накопившиеся сигналы доходят,
| вызывается реакция.
Если во время блокировки процессу было послано несколько одинаковых сигналов sig, то при разблокировании поступит только один. Поступление сигналов во время блокировки просто отмечается в специальной битовой шкале в паспорте процесса (примерно так):
mask |= (1 << (sig - 1));
и при разблокировании сигнала sig, если соответствующий бит выставлен, то приходит один такой сигнал (система вызывает функцию реакции). То есть sighold заставляет приходящие сигналы "накапливаться" в специальной маске, вместо того, чтобы немедленно вызывать реакцию на них. А sigrelse разрешает "накопившимся" сигналам (если они есть) прийти и вызывает реакцию на них. Функция
sigset(sig, react);
аналогична функции signal, за исключением того, что на время работы обработчика сигнала react, приход сигнала sig блокируется; то есть перед вызовом react как бы делается sighold, а при выходе из обработчика - sigrelse. Это значит, что если во время работы обработчика сигнала придет такой же сигнал, то программа не будет убита, а "запомнит" пришедший сигнал, и обработчик будет вызван повторно (когда сработает sigrelse).
Функция
sigpause(sig);
вызывается внутри "рамки"
sighold(sig);
...
sigpause(sig);
...
sigrelse(sig);
и вызывает задержку выполнения процесса до прихода сигнала sig. Функция разрешает приход сигнала sig (обычно на него должна быть задана реакция при помощи sigset), и "засыпает" до прихода сигнала sig.
В UNIX стандарта POSIX для управления сигналами есть вызовы sigaction, sigproc- mask, sigpending, sigsuspend. Посмотрите в документацию!
6.4.1. Напишите программу, выдающую на экран файл /etc/termcap. Перехватывайте сигнал SIGINT, при получении сигнала запрашивайте "Продолжать?". По ответу 'y' - продолжить выдачу; по 'n' - завершить программу; по 'r' - начать выдавать файл с начала: lseek(fd,0L,0). Не забудьте заново переустановить реакцию на SIGINT, поскольку после получения сигнала реакция автоматически сбрасывается.
#include <signal.h>
void onintr(sig){ /* sig - номер сигнала */
signal (sig, onintr); /* восстановить реакцию */
... запрос и действия ...
}
main(){ signal (SIGINT, onintr); ... }
Сигнал прерывания можно игнорировать. Это делается так:
signal (SIGINT, SIG_IGN);
Такую программу нельзя прервать с клавиатуры. Напомним, что реакция SIG_IGN сохраняется при приходе сигнала.
6.4.2. Системный вызов, находящийся в состоянии ожидания какого-то события (read ждущий нажатия кнопки на клавиатуре, wait ждущий окончания процесса-потомка, и.т.п.), может быть прерван сигналом. При этом сисвызов вернет значение "ошибка" (-1) и errno станет равно EINTR. Это позволяет нам писать системные вызовы с выставлением тайма- ута: если событие не происходит в течение заданного времени, то завершить ожидание и прервать сисвызов. Для этой цели используется вызов alarm(sec), заказывающий посылку сигнала SIGALRM нашей программе через целое число sec секунд (0 - отменяет заказ):
#include <signal.h>
void (*oldaction)(); int alarmed;
/* прозвонил будильник */
void onalarm(nsig){ alarmed++; }
...
/* установить реакцию на сигнал */
oldaction = signal (SIGALRM, onalarm);
/* заказать будильник через TIMEOUT сек. */
alarmed = 0; alarm ( TIMEOUT /* sec */ );
sys_call(...); /* ждет события */
// если нас сбил сигнал, то по сигналу будет
// еще вызвана реакция на него - onalarm
if(alarmed){
// событие так и не произошло.
// вызов прерван сигналом т.к. истекло время.
}else{
alarm(0); /* отменить заказ сигнала */
// событие произошло, сисвызов успел
// завершиться до истечения времени.
}
signal (SIGALRM, oldaction);
Напишите программу, которая ожидает ввода с клавиатуры в течение 10 секунд. Если ничего не введено - печатает "Нет ввода", иначе - печатает "Спасибо". Для ввода можно использовать как вызов read, так и функцию gets (или getchar), поскольку функция эта все равно внутри себя издает системный вызов read. Исследуйте, какое значение возвращает fgets (gets) в случае прерывания ее системным вызовом.
/* Копирование стандартного ввода на стандартный вывод
* с установленным тайм-аутом.
* Это позволяет использовать программу для чтения из FIFO-файлов
* и с клавиатуры.
* Небольшая модификация позволяет использовать программу
* для копирования "растущего" файла (т.е. такого, который в
* настоящий момент еще продолжает записываться).
* Замечание:
* В ДЕМОС-2.2 сигнал НЕ сбивает чтение из FIFO-файла,
* а получение сигнала откладывается до выхода из read()
* по успешному чтению информации. Пользуйтесь open()-ом
* с флагом O_NDELAY, чтобы получить требуемый эффект.
*
* Вызов: a.out /dev/tty
*
* По мотивам книги М.Дансмура и Г.Дейвиса.
*/
#define WAIT_TIME 5 /* ждать 5 секунд */
#define MAX_TRYS 5 /* максимум 5 попыток */
#define BSIZE 256
#define STDIN 0 /* дескриптор стандартного ввода */
#define STDOUT 1 /* дескриптор стандартного вывода */
#include <signal.h>
#include <errno.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
char buffer [ BSIZE ];
extern int errno; /* код ошибки */
void timeout(nsig){ signal( SIGALRM, timeout ); }
void main(argc, argv) char **argv;{
int fd, n, trys = 0; struct stat stin, stout;
if( argc != 2 ){
fprintf(stderr, "Вызов: %s файл\n", argv[0]); exit(1);
}
if((fd = !strcmp(argv[1],"-")? STDIN : open(argv[1],O_RDONLY)) < 0){
fprintf(stderr, "Не могу читать %s\n", argv[1]); exit(2);
}
/* Проверить, что ввод не совпадает с выводом,
* hardcat aFile >> aFile
* кроме случая, когда вывод - терминал.
* Такая проверка полезна для программ-фильтров (STDIN->STDOUT),
* чтобы исключить порчу исходной информации */
fstat(fd, &stin); fstat(STDOUT, &stout);
if( !isatty(STDOUT) && stin.st_ino == stout.st_ino &&
stin.st_dev == stout.st_dev
){ fprintf(stderr,
"\aВвод == выводу, возможно потеряна информация в %s.\n",argv[1]);
exit(33);
}
signal( SIGALRM, timeout );
while( trys < MAX_TRYS ){
alarm( WAIT_TIME ); /* заказать сигнал через 5 сек */
/* и ждем ввода ... */
n = read( fd, buffer, BSIZE );
alarm(0); /* отменили заказ сигнала */
/* (хотя, возможно, он уже получен) */
/* проверяем: почему мы слезли с вызова read() ? */
if( n < 0 && errno == EINTR ){
/* Мы были сбиты сигналом SIGALRM,
* код ошибки EINTR - сисвызов прерван
* неким сигналом.
*/
fprintf( stderr, "\7timed out (%d раз)\n", ++trys );
continue;
}
if( n < 0 ){
/* ошибка чтения */
fprintf( stderr, "read error.\n" ); exit(4);
}
if( n == 0 ){
/* достигнут конец файла */
fprintf( stderr, "Достигнут EOF.\n\n" ); exit(0);
}
/* копируем прочитанную информацию */
write( STDOUT, buffer, n );
trys = 0;
}
fprintf( stderr, "Все попытки провалились.\n" ); exit(5);
}
Если мы хотим, чтобы сисвызов не мог прерываться сигналом, мы должны защитить его:
#include <signal.h>
void (*fsaved)();
...
fsaved = signal (sig, SIG_IGN);
sys_call(...);
signal (sig, fsaved);
или так:
sighold(sig);
sys_call(...);
sigrelse(sig);
Сигналами могут быть прерваны не все системные вызовы и не при всех обстоятельствах.
6.4.3. Напишите функцию sleep(n), задерживающую выполнение программы на n секунд. Воспользуйтесь системным вызовом alarm(n) (будильник) и вызовом pause(), который задерживает программу до получения любого сигнала. Предусмотрите рестарт при получении во время ожидания другого сигнала, нежели SIGALRM. Сохраняйте заказ alarm, сделанный до вызова sleep (alarm выдает число секунд, оставшееся до завершения предыдущего заказа). На самом деле есть такая СТАНДАРТНАЯ функция. Ответ:
#include <sys/types.h>
#include <stdio.h>
#include <signal.h>
int got; /* пришел ли сигнал */
void onalarm(int sig)
{ printf( "Будильник\n" ); got++; } /* сигнал получен */
void sleep(int n){
time_t time(), start = time(NULL);
void (*save)();
int oldalarm, during = n;
if( n <= 0 ) return;
got = 0;
save = signal(SIGALRM, onalarm);
oldalarm = alarm(3600); /* Узнать старый заказ */
if( oldalarm ){
printf( "Был заказан сигнал, который придет через %d сек.\n",
oldalarm );
if(oldalarm > n) oldalarm -= n;
else { during = n = oldalarm; oldalarm = 1; }
}
printf( "n=%d oldalarm=%d\n", n, oldalarm );
while( n > 0 ){
printf( "alarm(%d)\n", n );
alarm(n); /* заказать SIGALRM через n секунд */
pause();
if(got) break;
/* иначе мы сбиты с pause другим сигналом */
n = during - (time(NULL) - start); /* прошло времени */
}
printf( "alarm(%d) при выходе\n", oldalarm );
alarm(oldalarm); /* alarm(0) - отмена заказа сигнала */
signal(SIGALRM, save); /* восстановить реакцию */
}
void onintr(int nsig){
printf( "Сигнал SIGINT\n"); signal(SIGINT, onintr);
}
void onOldAlarm(int nsig){
printf( "Звонит старый будильник\n");
}
void main(){
int time1 = 0; /* 5, 10, 20 */
setbuf(stdout, NULL);
signal(SIGINT, onintr);
signal(SIGALRM, onOldAlarm); alarm(time1);
sleep(10);
if(time1) pause();
printf("Чао!\n");
}
6.4.4. Напишите "часы", выдающие текущее время каждые 3 секунды.
#include <signal.h>
#include <time.h>
#include <stdio.h>
void tick(nsig){
time_t tim; char *s;
signal (SIGALRM, tick);
alarm(3); time(&tim);
s = ctime(&tim);
s[ strlen(s)-1 ] = '\0'; /* обрубить '\n' */
fprintf(stderr, "\r%s", s);
}
main(){ tick(0);
for(;;) pause();
}
6.5.1. Какие классы памяти имеют данные, в каких сегментах программы они расположены?
char x[] = "hello";
int y[25];
char *p;
main(){
int z = 12;
int v;
static int w = 25;
static int q;
char s[20];
char *pp;
...
v = w + z; /* #1 */
}
Ответ:
Переменная Класс памяти Сегмент Начальное значение
x static data/DATA "hello"
y static data/BSS {0, ..., 0}
p static data/BSS NULL
z auto stack 12
v auto stack не определено
w static data/DATA 25
q static data/BSS 0
s auto stack не определено
pp auto stack не определено
main static text/TEXT
Большими буквами обозначены сегменты, хранимые в выполняемом файле:
DATA - это инициализированные статические данные (которым присвоены начальные значения). Они помещаются компилятором в файл в виде готовых констант, а при запуске программы (при ее загрузке в память машины), просто копируются в память из файла.
BSS (Block Started by Symbol) - неинициализированные статические данные. Они по умолчанию имеют начальное значение 0 (NULL, "", '\0'). Эта память расписывается нулями при запуске программы, а в файле хранится лишь ее размер.
TEXT - сегмент, содержащий машинные команды (код).
Хранящаяся в файле выполняемая программа имеет также заголовок - в нем в частности содержатся размеры перечисленных сегментов и их местоположение в файле; и еще - в самом конце файла - таблицу имен. В ней содержатся имена всех функций и переменных, используемых в программе, и их адреса. Эта таблица используется отладчиками adb и sdb, а также при сборке программы из нескольких объектных файлов программой ld. Просмотреть ее можно командой
nm имяФайла
Для экономии дискового пространства эту таблицу часто удаляют, что делается командой
strip имяФайла
Размеры сегментов можно узнать командой
size имяФайла
Программа, загруженная в память компьютера (т.е. процесс), состоит из 3x сегментов, относящихся непосредственно к программе:
stack - стек для локальных переменных функций (автоматических переменных). Этот сегмент существует только у выполняющейся программы, поскольку отведение памяти в стеке производится выполнением некоторых машинных команд (поэтому описание автоматических переменных в Си - это на самом деле выполняемые операторы, хотя и не с точки зрения языка). Сегмент стека автоматически растет по мере надобности (если мы вызываем новые и новые функции, отводящие переменные в стеке). За этим следит аппаратура диспетчера памяти.
data - сегмент, в который склеены сегменты статических данных DATA и BSS, загруженные из файла. Этот сегмент также может изменять свой размер, но делать это надо явно - системными вызовами sbrk или brk. В частности, функция malloc() для размещения динамически отводимых данных увеличивает размер этого сегмента.
text - это выполняемые команды, копия сегмента TEXT из файла. Так строка с меткой #1 содержится в виде машинных команд именно в этом сегменте.
Кроме того, каждый процесс имеет еще:
proc - это резидентная часть паспорта процесса в таблице процессов в ядре операционной системы;
user - это 4-ый сегмент процесса - нерезидентная часть паспорта (u-area). К этому сегменту имеет доступ только ядро, но не сама программа.
Паспорт процесса был поделен на 2 части только из соображений экономии памяти в ядре: контекст процесса (таблица открытых файлов, ссылка на I-узел текущего каталога, таблица реакций на сигналы, ссылка на I-узел управляющего терминала, и.т.п.) нужен ядру только при обслуживании текущего активного процесса. Когда активен другой процесс эта информация в памяти ядра не нужна. Более того, если процесс из-за нехватки места в памяти машины был откачан на диск, эта информация также может быть откачана на диск и подкачана назад лишь вместе с процессом. Поэтому контекст был выделен в отдельный сегмент, и сегмент этот подключается к адресному пространству ядра лишь при выполнении процессом какого-либо системного вызова (это подключение называется "переключение контекста" - context switch). Четыре сегмента процесса могут располагаться в памяти машины не обязательно подряд - между ними могут лежать сегменты других процессов.
Схема составных частей процесса:
П Р О Ц Е С С
таблица процессов:
паспорт в ядре сегменты в памяти
struct proc[]
####---------------> stack 1
#### data 2
text 3
контекст: struct user 4
Каждый процесс имеет уникальный номер, хранящийся в поле p_pid в структуре proc*. В ней также хранятся: адреса сегментов процесса в памяти машины (или на диске, если процесс откачан); p_uid - номер владельца процесса; p_ppid - номер процесса-родителя; p_pri, p_nice - приоритеты процесса; p_pgrp - группа процесса; p_wchan - ожидаемое процессом событие; p_flag и p_stat - состояние процесса; и многое другое. Структура proc определена в include-файле <sys/proc.h>, а структура user - в <sys/user.h>.
6.5.2. Системный вызов fork() (вилка) создает новый процесс: копию процесса, издавшего вызов. Отличие этих процессов состоит только в возвращаемом fork-ом значении:
0 - в новом процессе.
pid нового процесса - в исходном.
Вызов fork может завершиться неудачей если таблица процессов переполнена. Простейший способ сделать это:
main(){
while(1)
if( ! fork()) pause();
}
Одно гнездо таблицы процессов зарезервировано - его может использовать только суперпользователь (в целях жизнеспособности системы: хотя бы для того, чтобы запустить программу, убивающую все эти процессы-варвары).
Вызов fork создает копию всех 4х сегментов процесса и выделяет порожденному процессу новый паспорт и номер. Иногда сегмент text не копируется, а используется процессами совместно ("разделяемый сегмент") в целях экономии памяти. При копировании сегмента user контекст порождающего процесса наследуется порожденным процессом (см. ниже).
Проведите опыт, доказывающий что порожденный системным вызовом fork() процесс и породивший его - равноправны. Повторите несколько раз программу:
#include <stdio.h>
int pid, i, fd; char c;
main(){
fd = creat( "TEST", 0644);
if( !(pid = fork())){ /* сын: порожденный процесс */
c = 'a';
for(i=0; i < 5; i++){
write(fd, &c, 1); c++; sleep(1);
}
printf("Сын %d окончен\n", getpid());
exit(0);
}
/* else процесс-отец */
c = 'A';
for(i=0; i < 5; i++){
write(fd, &c, 1); c++; sleep(1);
}
printf("Родитель %d процесса %d окончен\n",
getpid(), pid );
}
В файле TEST мы будем от случая к случаю получать строки вида
aABbCcDdEe или AaBbcdCDEe
что говорит о том, что первым "проснуться" после fork() может любой из двух процессов. Если же опыт дает устойчиво строки, начинающиеся с одной и той же буквы - значит в данной реализации системы один из процессов все же запускается раньше. Но не стоит использовать этот эффект - при переносе на другую систему его может не быть!
Данный опыт основан на следующем свойстве системы UNIX: при системном вызове fork() порожденный процесс получает все открытые порождающим процессом файлы "в наследство" - это соответствует тому, что таблица открытых процессом файлов копируется в процесс-потомок. Именно так, в частности, передаются от отца к сыну стандартные каналы 0, 1, 2: порожденному процессу не нужно открывать стандартные ввод, вывод и вывод ошибок явно. Изначально же они открываются специальной программой при вашем входе в систему.
до вызова fork();
таблица открытых
файлов процесса
0 ## ---<--- клавиатура
1 ## --->--- дисплей
2 ## --->--- дисплей
... ##
fd ## --->--- файл TEST
... ##
после fork();
ПРОЦЕСС-ПАПА ПРОЦЕСС-СЫН
0 ## ---<--- клавиатура --->--- ## 0
1 ## --->--- дисплей ---<--- ## 1
2 ## --->--- дисплей ---<--- ## 2
... ## ## ...
fd ## --->--- файл TEST ---<--- ## fd
... ## | ## ...
*--RWptr-->ФАЙЛ
Ссылки из таблиц открытых файлов в процессах указывают на структуры "открытый файл" в ядре (см. главу про файлы). Таким образом, два процесса получают доступ к одной и той же структуре и, следовательно, имеют общий указатель чтения/записи для этого файла. Поэтому, когда процессы "отец" и "сын" пишут по дескриптору fd, они пользуются одним и тем же указателем R/W, т.е. информация от обоих процессов записывается последовательно. На принципе наследования и совместного использования открытых файлов основан также системный вызов pipe.
Порожденный процесс наследует также: реакции на сигналы (!!!), текущий каталог, управляющий терминал, номер владельца процесса и группу владельца, и.т.п.
При системном вызове exec() (который заменяет программу, выполняемую процессом, на программу из указанного файла) все открытые каналы также достаются в наследство новой программе (а не закрываются).
6.5.3. Процесс-копия это хорошо, но не совсем то, что нам хотелось бы. Нам хочется запустить программу, содержащуюся в выполняемом файле (например a.out). Для этого существует системный вызов exec, который имеет несколько разновидностей. Рассмотрим только две:
char *path;
char *argv[], *envp[], *arg0, ..., *argn;
execle(path, arg0, arg1, ..., argn, NULL, envp);
execve(path, argv, envp);
Системный вызов exec заменяет программу, выполняемую данным процессом, на программу, загружаемую из файла path. В данном случае path должно быть полным именем файла или именем файла от текущего каталога:
/usr/bin/vi a.out ../mybin/xkick
Файл должен иметь код доступа "выполнение". Первые два байта файла (в его заголовке), рассматриваемые как short int, содержат так называемое "магическое число" (A_MAGIC), свое для каждого типа машин (смотри include-файл <a.out.h>). Его помещает в начало выполняемого файла редактор связей ld при компоновке программы из объектных файлов. Это число должно быть правильным, иначе система откажется запускать программу из этого файла. Бывает несколько разных магических чисел, обозначающих разные способы организации программы в памяти. Например, есть вариант, в котором сегменты text и data склеены вместе (тогда text не разделяем между процессами и не защищен от модификации программой), а есть - где данные и текст находятся в раздельных адресных пространствах и запись в text запрещена (аппаратно).
Остальные аргументы вызова - arg0, ..., argn - это аргументы функции main новой программы. Во второй форме вызова аргументы не перечисляются явно, а заносятся в массив. Это позволяет формировать произвольный массив строк-аргументов во время работы программы:
char *argv[20];
argv[0]="ls"; argv[1]="-l"; argv[2]="-i"; argv[3]=NULL;
execv( "/bin/ls", argv);
либо
execl( "/bin/ls", "ls","-l","-i", NULL):
В результате этого вызова текущая программа завершается (но не процесс!) и вместо нее запускается программа из заданного файла: сегменты stack, data, text старой программы уничтожаются; создаются новые сегменты data и text, загружаемые из файла path; отводится сегмент stack (первоначально - не очень большого размера); сегмент user сохраняется от старой программы (за исключением реакций на сигналы, отличных от SIG_DFL и SIG_IGN - они будут сброшены в SIG_DFL). Затем будет вызвана функция main новой программы с аргументами argv:
void main( argc, argv )
int argc; char *argv[]; { ... }
Количество аргументов - argc - подсчитает сама система. Строка NULL не подсчитывается.
Процесс остается тем же самым - он имеет тот же паспорт (только адреса сегментов изменились); тот же номер (pid); все открытые прежней программой файлы остаются открытыми (с теми же дескрипторами); текущий каталог также наследуется от старой программы; сигналы, которые игнорировались ею, также будут игнорироваться (остальные сбрасываются в SIG_DFL). Зато "сущность" процесса подвергается перерождению - он выполняет теперь иную программу. Таким образом, системный вызов exec осуществляет вызов функции main, находящейся в другой программе, передавая ей свои аргументы в качестве входных.
Системный вызов exec может не удаться, если указанный файл path не существует, либо вы не имеете права его выполнять (такие коды доступа), либо он не является выполняемой программой (неверное магическое число), либо слишком велик для данной машины (системы), либо файл открыт каким-нибудь процессом (например еще записывается компилятором). В этом случае продолжится выполнение прежней программы. Если же вызов успешен - возврата из exec не происходит вообще (поскольку управление передается в другую программу).
Аргумент argv[0] обычно полагают равным path. По нему программа, имеющая несколько имен (в файловой системе), может выбрать ЧТО она должна делать. Так программа /bin/ls имеет альтернативные имена lr, lf, lx, ll. Запускается одна и та же программа, но в зависимости от argv[0] она далее делает разную работу.
Аргумент envp - это "окружение" программы (см. начало этой главы). Если он не задан - передается окружение текущей программы (наследуется содержимое массива, на который указывает переменная environ); если же задан явно (например, окружение скопировано в какой-то массив и часть переменных подправлена или добавлены новые переменные) - новая программа получит новое окружение. Напомним, что окружение можно прочесть из предопределенной переменной char **environ, либо из третьего аргумента функции main (см. начало главы), либо функцией getenv().
Системные вызовы fork и exec не склеены в один вызов потому, что между fork и exec в процессе-сыне могут происходить некоторые действия, нарушающие симметрию процесса-отца и порожденного процесса: установка реакций на сигналы, перенаправление ввода/вывода, и.т.п. Смотри пример "интерпретатор команд" в приложении. В MS DOS, не имеющей параллельных процессов, вызовы fork, exec и wait склеены в один вызов spawn. Зато при этом приходится делать перенаправления ввода-вывода в порождающем процессе перед spawn, а после него - восстанавливать все как было.
6.5.4. Завершить процесс можно системным вызовом
void exit( unsigned char retcode );
Из этого вызова не бывает возврата. Процесс завершается: сегменты stack, data, text, user уничтожаются (при этом все открытые процессом файлы закрываются); память, которую они занимали, считается свободной и в нее может быть помещен другой процесс. Причина смерти отмечается в паспорте процесса - в структуре proc в таблице процессов внутри ядра. Но паспорт еще не уничтожается! Это состояние процесса называется "зомби" - живой мертвец.
В паспорт процесса заносится код ответа retcode. Этот код может быть прочитан процессом-родителем (тем, кто создал этот процесс вызовом fork). Принято, что код 0 означает успешное завершение процесса, а любое положительное значение 1..255 означает неудачное завершение с таким кодом ошибки. Коды ошибок заранее не предопределены: это личное дело процессов отца и сына - установить между собой какие-то соглашения по этому поводу. В старых программах иногда писалось exit(-1); Это некорректно - код ответа должен быть неотрицателен; код -1 превращается в код 255. Часто используется конструкция exit(errno);
Программа может завершиться не только явно вызывая exit, но и еще двумя способами:
6.5.5. В действительности exit() - это еще не сам системный вызов завершения, а стандартная функция. Сам системный вызов называется _exit(). Мы можем переопределить функцию exit() так, чтобы по окончании программы происходили некоторые действия:
void exit(unsigned code){
/* Добавленный мной дополнительный оператор: */
printf("Закончить работу, "
"код ответа=%u\n", code);
/* Стандартные операторы: */
_cleanup(); /* закрыть все открытые файлы.
* Это стандартная функция **/
_exit(code); /* собственно сисвызов */
}
int f(){ return 17; }
void main(){
printf("aaaa\n"); printf("bbbb\n"); f();
/* потом откомментируйте это: exit(77); */
}
Здесь функция exit вызывается неявно по окончании main, ее подставляет в программу компилятор. Дело в том, что при запуске программы exec-ом, первым начинает выполняться код так называемого "стартера", подклеенного при сборке программы из файла /lib/crt0.o. Он выглядит примерно так (в действительности он написан на ассемблере):
... // вычислить argc, настроить некоторые параметры.
main(argc, argv, envp);
exit();
или так (взято из проекта GNU* -):
int errno = 0;
char **environ;
_start(int argc, int arga)
{
/* OS and Compiler dependent!!!! */
char **argv = (char **) &arga;
char **envp = environ = argv + argc + 1;
/* ... возможно еще какие-то инициализации,
* наподобие setlocale( LC_ALL, "" ); в SCO UNIX */
exit (main(argc, argv, envp));
}
Где должно быть
int main(int argc, char *argv[], char *envp[]){
...
return 0; /* вместо exit(0); */
}
Адрес функции _start() помечается в одном из полей заголовка файла формата a.out как адрес, на который система должна передать управление после загрузки программы в память (точка входа).
Какой код ответа попадет в exit() в этих примерах (если отсутствует явный вызов exit или return) - непредсказуемо. На IBM PC в вышенаписанном примере этот код равен 17, то есть значению, возвращенному последней вызывавшейся функцией. Однако это не какое-то специальное соглашение, а случайный эффект (так уж устроен код, создаваемый этим компилятором).
6.5.6. Процесс-отец может дождаться окончания своего потомка. Это делается системным вызовом wait и нужно по следующей причине: пусть отец - это интерпретатор команд. Если он запустил процесс и продолжил свою работу, то оба процесса будут предпринимать попытки читать ввод с клавиатуры терминала - интерпретатор ждет команд, а запущенная программа ждет данных. Кому из них будет поступать набираемый нами текст - непредсказуемо! Вывод: интерпретатор команд должен "заснуть" на то время, пока работает порожденный им процесс:
int pid; unsigned short status;
...
if((pid = fork()) == 0 ){
/* порожденный процесс */
... // перенаправления ввода-вывода.
... // настройка сигналов.
exec(....);
perror("exec не удался"); exit(1);
}
/* иначе это породивший процесс */
while((pid = wait(&status)) > 0 )
printf("Окончился сын pid=%d с кодом %d\n",
pid, status >> 8);
printf( "Больше нет сыновей\n");
wait приостанавливает* - выполнение вызвавшего процесса до момента окончания любого из порожденных им процессов (ведь можно было запустить и нескольких сыновей!). Как только какой-то потомок окончится - wait проснется и выдаст номер (pid) этого потомка. Когда никого из живых "сыновей" не осталось - он выдаст (-1). Ясно, что процессы могут оканчиваться не в том порядке, в котором их порождали. В переменную status заносится в специальном виде код ответа окончившегося процесса, либо номер сигнала, которым он был убит.
#include <sys/types.h>
#include <sys/wait.h>
...
int status, pid;
...
while((pid = wait(&status)) > 0){
if( WIFEXITED(status)){
printf( "Процесс %d умер с кодом %d\n",
pid, WEXITSTATUS(status));
} else if( WIFSIGNALED(status)){
printf( "Процесс %d убит сигналом %d\n",
pid, WTERMSIG(status));
if(WCOREDUMP(status)) printf( "Образовался core\n" );
/* core - образ памяти процесса для отладчика adb */
} else if( WIFSTOPPED(status)){
printf( "Процесс %d остановлен сигналом %d\n",
pid, WSTOPSIG(status));
} else if( WIFCONTINUED(status)){
printf( "Процесс %d продолжен\n",
pid);
}
}
...
Если код ответа нас не интересует, мы можем писать wait(NULL).
Если у нашего процесса не было или больше нет живых сыновей - вызов wait ничего не ждет, а возвращает значение (-1). В написанном примере цикл while позволяет дождаться окончания всех потомков.
В тот момент, когда процесс-отец получает информацию о причине смерти потомка, паспорт умершего процесса наконец вычеркивается из таблицы процессов и может быть переиспользован новым процессом. До того, он хранится в таблице процессов в состоянии "zombie" - "живой мертвец". Только для того, чтобы кто-нибудь мог узать статус его завершения.
Если процесс-отец завершился раньше своих сыновей, то кто же сделает wait и вычеркнет паспорт? Это сделает процесс номер 1: /etc/init. Если отец умер раньше процессов-сыновей, то система заставляет процесс номер 1 "усыновить" эти процессы. init обычно находится в цикле, содержащем в начале вызов wait(), то есть ожидаетокончания любого из своих сыновей (а они у него всегда есть, о чем мы поговорим подробнее чуть погодя). Таким образом init занимается чисткой таблицы процессов, хотя это не единственная его функция.
Вот схема, поясняющая жизненный цикл любого процесса:
|pid=719,csh
|
if(!fork())------->--------* pid=723,csh
| | загрузить
wait(&status) exec("a.out",...) <-- a.out
: main(...){ с диска
: |
:pid=719,csh | pid=723,a.out
спит(ждет) работает
: |
: exit(status) умер
: }
проснулся <---проснись!--RIP
|
|pid=719,csh
Заметьте, что номер порожденного процесса не обязан быть следующим за номером родителя, а только больше него. Это связано с тем, что другие процессы могли создать в системе новые процессы до того, как наш процесс издал свой вызов fork.
6.5.7. Кроме того, wait позволяет отслеживать остановку процесса. Процесс может быть приостановлен при помощи посылки ему сигналов SIGSTOP, SIGTTIN, SIGTTOU, SIGTSTP. Последние три сигнала посылает при определенных обстоятельствах драйвер терминала, к примеру SIGTSTP - при нажатии клавиши CTRL/Z. Продолжается процесс посылкой ему сигнала SIGCONT.
В данном контексте, однако, нас интересуют не сами эти сигналы, а другая схема манипуляции с отслеживанием статуса порожденных процессов. Если указано явно, система может посылать процессу-родителю сигнал SIGCLD в момент изменения статуса любого из его потомков. Это позволит процессу-родителю немедленно сделать wait и немедленно отразить изменение состояние процесса-потомка в своих внутренних списках. Данная схема программируется так:
void pchild(){
int pid, status;
sighold(SIGCLD);
while((pid = waitpid((pid_t) -1, &status, WNOHANG|WUNTRACED)) > 0){
dorecord:
записать_информацию_об_изменениях;
}
sigrelse(SIGCLD);
/* Reset */
signal(SIGCLD, pchild);
}
...
main(){
...
/* По сигналу SIGCLD вызывать функцию pchild */
signal(SIGCLD, pchild);
...
главный_цикл;
}
Секция с вызовом waitpid (разновидность вызова wait), прикрыта парой функций sighold-sigrelse, запрещающих приход сигнала SIGCLD внутри этой критической секции. Сделано это вот для чего: если процесс начнет модифицировать таблицы или списки в районе метки dorecord:, а в этот момент придет еще один сигнал, то функция pchild будет вызвана рекурсивно и тоже попытается модифицировать таблицы и списки, в которых еще остались незавершенными перестановки ссылок, элементов, счетчиков. Это приведет к разрушению данных.
Поэтому сигналы должны приходить последовательно, и функции pchild вызываться также последовательно, а не рекурсивно. Функция sighold откладывает доставку сигнала (если он случится), а sigrelse - разрешает доставить накопившиеся сигналы (но если их пришло несколько одного типа - все они доставляются как один такой сигнал. Отсюда цикл вокруг waitpid).
Флаг WNOHANG - означает "не ждать внутри вызова wait", если ни один из потомков не изменил своего состояния; а просто вернуть код (-1)". Это позволяет вызывать pchild даже без получения сигнала: ничего не произойдет. Флаг WUNTRACED - означает "выдавать информацию также об остановленных процессах".
* - Процесс может узнать его вызовом pid=getpid();
* - cleanup() закрывает файлы, открытые fopen()ом, "вытряхая" при этом данные, накопленные в буферах, в файл. При аварийном завершении программы файлы все равно закрываются, но уже не явно, а операционной системой (в вызове _exit). При этом содержимое недосброшенных буферов будет утеряно.
* - программы, распространяемые в исходных текстах из Free Software Foundationtion (FSF). Среди них - C++ компилятор g++ и редактор emacs. Смысл слов GNU - "generally not UNIX" - проект был основан как противодействие начавшейся коммерциализации UNIX и закрытию его исходных текстов. "Сделать как в UNIX, но лучше".
* - "Живой" процесс может пребывать в одном из нескольких состояний: процесс ожидает наступления какого-то события ("спит"), при этом ему не выделяется время процессора, т.к. он не готов к выполнению; процесс готов к выполнению и стоит в очереди к процессору (поскольку процессор выполняет другой процесс); процесс готов и выполняется процессором в данный момент. Последнее состояние может происходить в двух режимах пользовательском (выполняются команды сегмента text) и системном (процессом был издан системный вызов, и сейчас выполняется функция в ядре). Ожидание события бывает только в системной фазе - внутри системного вызова (т.е. это "синхронное" ожидание). Неактивные процессы ("спящие" или ждущие ресурса процессора) могут быть временно откачаны на диск.
6.5.8. Как уже было сказано, при exec все открытые файлы достаются в наследство новой программе (в частности, если между fork и exec были перенаправлены вызовом dup2 стандартные ввод и вывод, то они останутся перенаправленными и у новой программы). Что делать, если мы не хотим, чтобы наследовались все открытые файлы? (Хотя бы потому, что большинством из них новая программа пользоваться не будет - в основном она будет использовать лишь fd 0, 1 и 2; а ячейки в таблице открытых файлов процесса они занимают). Во-первых, ненужные дескрипторы можно явно закрыть close в промежутке между fork-ом и exec-ом. Однако не всегда мы помним номера дескрипторов для этой операции. Более радикальной мерой является тотальная чистка:
for(f = 3; f < NOFILE; f++)
close(f);
Есть более элегантный путь. Можно пометить дескриптор файла специальным флагом, означающим, что во время вызова exec этот дескриптор должен быть автоматически закрыт (режим file-close-on-exec - fclex):
#include <fcntl.h>
int fd = open(.....);
fcntl (fd, F_SETFD, 1);
Отменить этот режим можно так:
fcntl (fd, F_SETFD, 0);
Здесь есть одна тонкость: этот флаг устанавливается не для структуры file - "открытый файл", а непосредственно для дескриптора в таблице открытых процессом файлов (массив флагов: char u_pofile[NOFILE]). Он не сбрасывается при закрытии файла, поэтому нас может ожидать сюрприз:
... fcntl (fd, F_SETFD, 1); ... close(fd);
...
int fd1 = open( ... );
Если fd1 окажется равным fd, то дескриптор fd1 будет при exec-е закрыт, чего мы явно не ожидали! Поэтому перед close(fd) полезно было бы отменить режим fclex.
6.5.9. Каждый процесс имеет управляющий терминал (short *u_ttyp). Он достается процессу в наследство от родителя (при fork и exec) и обычно совпадает с терминалом, с на котором работает данный пользователь.
Каждый процесс относится к некоторой группе процессов (int p_pgrp), которая также наследуется. Можно послать сигнал всем процессам указанной группы pgrp:
kill( -pgrp, sig );
Вызов
kill( 0, sig );
посылает сигнал sig всем процессам, чья группа совпадает с группой посылающего процесса. Процесс может узнать свою группу:
int pgrp = getpgrp();
а может стать "лидером" новой группы. Вызов
setpgrp();
делает следующие операции:
/* У процесса больше нет управл. терминала: */
if(p_pgrp != p_pid) u_ttyp = NULL;
/* Группа процесса полагается равной его ид-у: */
p_pgrp = p_pid; /* new group */
В свою очередь, управляющий терминал тоже имеет некоторую группу (t_pgrp). Это значение устанавливается равным группе процесса, первым открывшего этот терминал:
/* часть процедуры открытия терминала */
if( p_pid == p_pgrp // лидер группы
&& u_ttyp == NULL // еще нет упр.терм.
&& t_pgrp == 0 ){ // у терминала нет группы
u_ttyp = &t_pgrp;
t_pgrp = p_pgrp;
}
Таким процессом обычно является процесс регистрации пользователя в системе (который спрашивает у вас имя и пароль). При закрытии терминала всеми процессами (что бывает при выходе пользователя из системы) терминал теряет группу: t_pgrp=0;
При нажатии на клавиатуре терминала некоторых клавиш:
c_cc[ VINTR ] обычно DEL или CTRL/C
c_cc[ VQUIT ] обычно CTRL/\
драйвер терминала посылает соответственно сигналы SIGINT и SIGQUIT всем процессам группы терминала, т.е. как бы делает
kill( -t_pgrp, sig );
Именно поэтому мы можем прервать процесс нажатием клавиши DEL. Поэтому, если процесс сделал setpgrp(), то сигнал с клавиатуры ему послать невозможно (т.к. он имеет свой уникальный номер группы != группе терминала).
Если процесс еще не имеет управляющего терминала (или уже его не имеет после setpgrp), то он может сделать любой терминал (который он имеет право открыть) управляющим для себя. Первый же файл-устройство, являющийся интерфейсом драйвера терминалов, который будет открыт этим процессом, станет для него управляющим терминалом. Так процесс может иметь каналы 0, 1, 2 связанные с одним терминалом, а прерывания получать с клавиатуры другого (который он сделал управляющим для себя).
Процесс регистрации пользователя в системе - /etc/getty (название происходит от "get tty" - получить терминал) - запускается процессом номер 1 - /etc/init-ом - на каждом из терминалов, зарегистрированных в системе, когда
В сильном упрощении getty может быть описан так:
void main(ac, av) char *av[];
{ int f; struct termio tmodes;
for(f=0; f < NOFILE; f++) close(f);
/* Отказ от управляющего терминала,
* основание новой группы процессов.
*/
setpgrp();
/* Первоначальное явное открытие терминала */
/* При этом терминал av[1] станет упр. терминалом */
open( av[1], O_RDONLY ); /* fd = 0 */
open( av[1], O_RDWR ); /* fd = 1 */
f = open( av[1], O_RDWR ); /* fd = 2 */
// ... Считывание параметров терминала из файла
// /etc/gettydefs. Тип требуемых параметров линии
// задается меткой, указываемой в av[2].
// Заполнение структуры tmodes требуемыми
// значениями ... и установка мод терминала.
ioctl (f, TCSETA, &tmodes);
// ... запрос имени и пароля ...
chdir (домашний_каталог_пользователя);
execl ("/bin/csh", "-csh", NULL);
/* Запуск интерпретатора команд. Группа процессов,
* управл. терминал, дескрипторы 0,1,2 наследуются.
*/
}
Здесь последовательные вызовы open занимают последовательные ячейки в таблице открытых процессом файлов (поиск каждой новой незанятой ячейки производится с начала таблицы) - в итоге по дескрипторам 0,1,2 открывается файл-терминал. После этого дескрипторы 0,1,2 наследуются всеми потомками интерпретатора команд. Процесс init запускает по одному процессу getty на каждый терминал, как бы делая
/etc/getty /dev/tty01 m &
/etc/getty /dev/tty02 m &
...
и ожидает окончания любого из них. После входа пользователя в систему на каком-то терминале, соответствующий getty превращается в интерпретатор команд (pid процесса сохраняется). Как только кто-то из них умрет - init перезапустит getty на соответствующем терминале (все они - его сыновья, поэтому он знает - на каком именно терминале).
Процессы могут обмениваться между собой информацией через файлы. Существуют файлы с необычным поведением - так называемые FIFO-файлы (first in, first out), ведущие себя подобно очереди. У них указатели чтения и записи разделены. Работа с таким файлом напоминает проталкивание шаров через трубу - с одного конц