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); } А. Богатырев, 1992-95 - 211 - Си в UNIX 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, ... А. Богатырев, 1992-95 - 212 - Си в UNIX По два значения дано потому, что операционная система резервирует часть файловой сис- темы для использования ТОЛЬКО суперпользователем (чтобы администратор смог распихать файлы в случае переполнения диска, и имел резерв на это). ufs - это UNIX file system из BSD 4.x 6.4. Сигналы. Процессы в UNIX используют много разных механизмов взаимодействия. Одним из них являются сигналы. Сигналы - это асинхронные события. Что это значит? Сначала объясним, что такое синхронные события: я два раза в день подхожу к почтовому ящику и проверяю - нет ли в нем почты (событий). Во-первых, я произвожу опрос - "нет ли для меня события?", в программе это выглядело бы как вызов функции опроса и, может быть, ожидания события. Во-вторых, я знаю, что почта может ко мне прийти, поскольку я подписался на какие-то газеты. То есть я предварительно заказывал эти события. Схема с синхронными событиями очень распространена. Кассир сидит у кассы и ожи- дает, пока к нему в окошечко не заглянет клиент. Поезд периодически проезжает мимо светофора и останавливается, если горит красный. Функция Си пассивно "спит" до тех пор, пока ее не вызовут; однако она всегда готова выполнить свою работу (обслужить клиента). Такое ожидающее заказа (события) действующее лицо называется сервер. После выполнения заказа сервер вновь переходит в состояние ожидания вызова. Итак, если событие ожидается в специальном месте и в определенные моменты времени (издается некий вызов для ОПРОСА) - это синхронные события. Канонический пример - функция gets, которая задержит выполнение программы, пока с клавиатуры не будет введена строка. Большинство ожиданий внутри системных вызовов - синхронны. Ядро ОС высту- пает для программ пользователей в роли сервера, выполняющего сисвызовы (хотя и не только в этой роли - ядро иногда предпринимает и активные действия: передача процес- сора другому процессу через определенное время (режим разделения времени), убивание процесса при ошибке, и.т.п.). Сигналы - это асинхронные события. Они приходят неожиданно, в любой момент вре- мени - вроде телефонного звонка. Кроме того, их не требуется заказывать - сигнал процессу может поступить совсем без повода. Аналогия из жизни такова: человек сидит и пишет письмо. Вдруг его окликают посреди фразы - он отвлекается, отвечает на воп- рос, и вновь продолжает прерванное занятие. Человек не ожидал этого оклика (быть может, он готов к нему, но он не озирался по сторонам специально). Кроме того, сиг- нал мог поступить когда он писал 5-ое предложение, а мог - когда 34-ое. Момент вре- мени, в который произойдет прерывание, не фиксирован. Сигналы имеют номера, причем их количество ограничено - есть определенный список допустимых сигналов. Номера и мнемонические имена сигналов перечислены в include- файле <signal.h> и имеют вид SIGнечто. Допустимы сигналы с номерами 1..NSIG-1, где NSIG определено в этом файле. При получении сигнала мы узнаем его номер, но не узнаем никакой иной информации: ни от кого поступил сигнал, ни что от нас хотят. Просто "звонит телефон". Чтобы получить дополнительную информацию, наш процесс должен взять ее из другого известного места; например - прочесть заказ из некоторого файла, об имени которого все наши программы заранее "договорились". Сигналы процессу могут поступать тремя путями: - От другого процесса, который явно посылает его нам вызовом kill(pid, sig); где pid - идентификатор (номер) процесса-получателя, а sig - номер сигнала. Послать сигнал можно только родственному процессу - запущенному тем же пользова- телем. - От операционной системы. Система может посылать процессу ряд сигналов, сигнали- зирующих об ошибках, например при обращении программы по несуществующему адресу или при ошибочном номере системного вызова. Такие сигналы обычно прекращают наш процесс. - От пользователя - с клавиатуры терминала можно нажимом некоторых клавиш послать сигналы SIGINT и SIGQUIT. Собственно, сигнал посылается драйвером терминала при получении им с клавиатуры определенных символов. Так можно прервать зациклившу- юся или надоевшую программу. Процесс-получатель должен как-то отреагировать на сигнал. Программа может: А. Богатырев, 1992-95 - 213 - Си в UNIX - проигнорировать сигнал (не ответить на звонок); - перехватить сигнал (снять трубку), выполнить какие-то действия, затем продолжить прерванное занятие; - быть убитой сигналом (звонок был подкреплен броском гранаты в окно); В большинстве случаев сигнал по умолчанию убивает процесс-получатель. Однако процесс может изменить это умолчание и задать свою реакцию явно. Это делается вызовом signal: #include <signal.h> void (*signal(int sig, void (*react)() )) (); Параметр react может иметь значение: SIG_IGN сигнал sig будет отныне игнорироваться. Некоторые сигналы (например SIGKILL) невозможно перехватить или проигнорировать. SIG_DFL восстановить реакцию по умолчанию (обычно - смерть получателя). имя_функции Например 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 - + + смерть потомка - Сигнал SIGILL используется иногда для эмуляции команд с плавающей точкой, что происходит примерно так: при обнаружении "запрещенной" команды для отсутствую- щего процессора "плавающей" арифметики аппаратура дает прерывание и система посылает процессу сигнал SIGILL. По сигналу вызывается функция-эмулятор плаваю- щей арифметики (подключаемая к выполняемому файлу автоматически), которая и обрабатывает требуемую команду. Это может происходить много раз, именно поэтому А. Богатырев, 1992-95 - 214 - Си в UNIX реакция на этот сигнал не сбрасывается. - SIGALRM посылается в результате его заказа вызовом alarm() (см. ниже). - Сигнал SIGCLD посылается процессу-родителю при выполнении процессом-потомком сисвызова exit (или при смерти вследствие получения сигнала). Обычно процесс- родитель при получении такого сигнала (если он его заказывал) реагирует, выпол- няя в обработчике сигнала вызов wait (см. ниже). По-умолчанию этот сигнал игно- рируется. - Реакция SIG_IGN не сбрасывается в SIG_DFL при приходе сигнала, т.е. сигнал игно- рируется постоянно. - Вызов signal возвращает старое значение реакции, которое может быть запомнено в переменную вида void (*f)(); а потом восстановлено. - Синхронное ожидание (сисвызов) может иногда быть прервано асинхронным событием (сигналом), но об этом ниже. Некоторые версии 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); А. Богатырев, 1992-95 - 215 - Си в UNIX и вызывает задержку выполнения процесса до прихода сигнала 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), поскольку А. Богатырев, 1992-95 - 216 - Си в UNIX функция эта все равно внутри себя издает системный вызов 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); } А. Богатырев, 1992-95 - 217 - Си в UNIX 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 выдает число секунд, оставшееся до завершения предыду- щего заказа). На самом деле есть такая СТАНДАРТНАЯ функция. Ответ: А. Богатырев, 1992-95 - 218 - Си в UNIX #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"); } А. Богатырев, 1992-95 - 219 - Си в UNIX 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. Жизнь процессов. 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'). Эта память расписывается нулями при запуске прог- раммы, а в файле хранится лишь ее размер. А. Богатырев, 1992-95 - 220 - Си в UNIX 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 А. Богатырев, 1992-95 - 221 - Си в UNIX Каждый процесс имеет уникальный номер, хранящийся в поле 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() может любой из двух процес- сов. Если же опыт дает устойчиво строки, начинающиеся с одной и той же буквы - значит ____________________ |- Процесс может узнать его вызовом pid=getpid(); А. Богатырев, 1992-95 - 222 - Си в UNIX в данной реализации системы один из процессов все же запускается раньше. Но не стоит использовать этот эффект - при переносе на другую систему его может не быть! Данный опыт основан на следующем свойстве системы 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 А. Богатырев, 1992-95 - 223 - Си в UNIX Файл должен иметь код доступа "выполнение". Первые два байта файла (в его заго- ловке), рассматриваемые как 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 ос