Основы операционных систем

Изучение теорий операционных систем. Исследование принципов построения и особенностей проектирования современных ОС. Сущность виртуальной памяти и алгоритма синхронизации. Рассмотрение операционной и файловой системы, система управления вводом-выводом.

Рубрика Программирование, компьютеры и кибернетика
Вид книга
Язык русский
Дата добавления 12.01.2010
Размер файла 2,6 M

Отправить свою хорошую работу в базу знаний просто. Используйте форму, расположенную ниже

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

/* В случае ошибки пытаемся определить: возникла ли она из-за того, что сегмент разделяемой памяти уже существует или по другой причине */ if(errno ! = EEXIST) {

/* Если по другой причине - прекращаем работу */ printf("Can\'t create shared memory\n"); exit(-1); } else {

/* Если из-за того, что разделяемая память уже существует, то пытаемся получить ее IPC дескриптор и, в случае удачи, сбрасываем флаг необходимости инициализации элементов массива */

if((shmid = shmget(key, 3*sizeof(int), 0)) < 0){ printf("Can\'t find shared memory\n"); exit(-1);

}

new = 0;

}

/* Пытаемся отобразить разделяемую память в адресное пространство текущего процесса. Обратите внимание на то, что для правильного сравнения мы явно преобразовываем значение -1 к указателю на целое.*/ if((array = (int *)shmat(shmid, NULL, 0)) == (int *)(-l)){

printf("Can't attach shared memory\n");

exit(-1);

}

/* В зависимости от значения флага new либо инициализируем массив, либо увеличиваем соответствующие счетчики */ if(new){

array[0] = 1 ;

array[1] = 0;

array[2] = 1; } else {

array[0] += 1 ;

array[2] += 1;

}

/* Печатаем новые значения счетчиков, удаляем разделяемую память из адресного пространства текущего процесса и завершаем работу */ printf("Program 1 was spawn %d times,

program 2 - %d times, total - %d times\n",

array[0], array[1], array[2]); if(shmdt(array) < 0){

printf("Can't detach shared memory\n");

exit(-1);

}

return 0;

}

/* Программа 2 (06-lb.с) для иллюстрации работы с разделяемой памятью */

/* Мы организуем разделяемую память для массива из трех целых чисел. Первый элемент массива является счетчиком числа запусков программы 1, т. е. данной программы, второй элемент массива - счетчиком числа запусков программы 2, третий элемент массива -счетчиком числа запусков обеих программ */ #include <sys/types.h>

#include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> #include <errno.h> int main() {

int *array; /* Указатель на разделяемую память */ int shmid; /* IPC дескриптор для области

разделяемой памяти */ int new =1; /* Флаг необходимости инициализации

элементов массива */ char pathname!] = "06-la.c"; /* Имя файла,

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

именем должен существовать в текущей директории */ key_t key; /* IPC ключ */

/* Генерируем IPC ключ из имени файла 0б-1а.с в текущей директории и номера экземпляра области разделяемой памяти 0 */ if((key = ftok(pathname,0)) < 0){

printf("Can\'t generate key\n");

exit(-1);

}

/* Пытаемся эксклюзивно создать разделяемую память

для сгенерированного ключа, т. е. если для этого ключа она уже существует, системный вызов вернет отрицательное значение. Размер памяти определяем как размер массива из трех целых переменных, права доступа 0666 - чтение и запись разрешены для всех */ if((shmid = shmget(key, 3 *sizeof(int),

0666 IIPC_CREATIIPC_EXCL)) < 0){ /* В случае возникновения ошибки пытаемся определить: возникла ли она из-за того, что сегмент разделяемой памяти уже существует или по другой причине */

if(errno != EEXIST){

/* Если по другой причине - прекращаем работу */ printf("Can\'t create shared memory\n"); exit(-1) ;

} else {

/* Если из-за того, что разделяемая память уже существует, то пытаемся получить ее IPC дескриптор и, в случае удачи, сбрасываем флаг необходимости инициализации элементов массива */

if((shmid = shmget(key, 3*sizeof(int), 0)) < ОН printf{"Can\'t find shared memory\n"); exit(-1);

}

new = 0 ;

}

}

/* Пытаемся отобразить разделяемую память в адресное пространство текущего процесса. Обратите внимание на то, что для правильного сравнения мы явно преобразовываем значение -1 к указателю на целое.*/ if ((array = (int *)shmat(shmid, NULL, 0)) = = (int *) (-1) ) {

printf("Can't attach shared memory\n"); exit(-1);

}

/* В зависимости от значения флага new либо инициализируем массив, либо увеличиваем соответствующие счетчики */ if(new){

array[0] = 0 ;

array[1] = 1;

array[2] = 1; } else {

array[1] += 1;

array[2] += 1;

}

/* Печатаем новые значения счетчиков, удаляем разделяемую память из адресного пространства текущего процесса и завершаем работу */ printf("Program 1 was spawn %d times,

program 2 - %d times, total - %d times\n", array[0], array[1], array[2]); if(shmdt(array) < 0){

printf("Can't detach shared memory\n");

exit(-1) ;

}

return 0 ;

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

В разделяемой памяти размещается массив из трех целых чисел. Первый элемент массива используется как счетчик для программы 1, второй элемент -- для программы 2, третий элемент -- для обеих программ суммарно. Дополнительный нюанс в программах возникает из-за необходимости инициализации элементов массива при создании разделяемой памяти. Для этого нам нужно, чтобы программы могли различать случай, когда они создали ее, и случай, когда она уже существовала. Мы добиваемся различия, используя вначале системный вызов shmget () с флагами IPC_CREAT и IPC_EXCL. Если вызов завершается нормально, то мы создали разделяемую память. Если вызов завершается с констатацией ошибки и значение переменной ептю равняется EEXIST, то, значит, разделяемая память уже существует, и мы можем получить ее IPC-дескриптор, применяя тот же самый вызов с нулевым значением флагов. Наберите программы, сохраните под именами 06-1а.с и 06-1Ь.с соответственно, откомпилируйте их и запустите несколько раз. Проанализируйте полученные результаты.

Команды ipcs и ipcrm

Как мы видели из предыдущего примера, созданная область разделяемой памяти сохраняется в операционной системе даже тогда, когда нет ни одного процесса, включающего ее в свое адресное пространство. С одной стороны, это имеет определенные преимущества, поскольку не требует одновременного существования взаимодействующих процессов, с другой стороны, может причинять существенные неудобства. Допустим, что предыдущие программы мы хотим использовать таким образом, чтобы подсчитывать количество запусков в течение одного, текущего, сеанса работы в системе. Однако в созданном сегменте разделяемой памяти остается информация от предыдущего сеанса, и программы будут выдавать общее количество запусков за все время работы с момента загрузки операционной системы. Можно было бы создавать для нового сеанса новый сегмент разделяемой памяти, но количество ресурсов в системе не безгранично. Нас спасает то, что существуют способы удалять неиспользуемые ресурсы System V IPC как с помощью команд операционной системы, так и с помощью системных вызовов. Все средства System VI PC требуют определенных действий для освобождения занимаемых ресурсов после окончания взаимодействия процессов. Для того чтобы удалять ресурсы System V IPC из командной строки, нам понадобятся две команды, ipcs и ipcrm.

Команда ipcs выдает информацию обо всех средствах System V IPC, существующих в системе, для которых пользователь обладает правами на чтение: областях разделяемой памяти, семафорах и очередях сообщений.

Команда ipcs

Синтаксис команды

ipcs [-asmq] [-tclup] ipcs [-smq] -і id ipcs -h

Описание команды

Команда ipcs предназначена для получения информации о средствах System V IPC, к которым пользователь имеет право доступа на чтение.

Опция -і позволяет указать идентификатор ресурсов. Будет выдаваться только информация для ресурсов, имеющих этот идентификатор.

Вида IPC ресурсов могут быть заданы с пс«ющью следующих опций:

для семафоров;

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

-q для очередей сообщений;

для всех ресурсов (по умолчанию).

Опции [-tclup] иодьзуются для изменения состава выходной информации. По умолчанию для каждого средства выводятся его ключ, идентификатор IPC, идентификатор владельца, пршдослуга и ряд других характериш Применение опций гкшшет вывести:

-t времена совершения последних операций над средствами IPC;

идентификаторы процесса, созвавшего ресурс, и процесса, совершившего над ним гюследнюю операцию;

идентификаторы пшняжателя и групп* для сеедатега ресурса и

-1 сжлемные ограничения для средств SystemVIPC;

общее состояние IPC ресурсов в системе.

Опция -h используется для получения краткой справочной информации.

Из всего многообразия выводимой информации нас будут интересовать только IPC идентификаторы для средств, созданных вами. Эти идентификаторы будут использоваться в команде ipcrm, позволяющей удалить необходимый ресурс из системы. Для удаления сегмента разделяемой памяти эта команда имеет вид

ipcrm shm <1РС идентификатор>

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

Команда ipcrm

Синтаксис команды

ipcrm [shm I msg I sem] id Описание команды

Команда ipcrm предназначена для удаления ресурса System V IPC из операционной системы. Параметр id задает IPC-идентификатор для удаляемого ресурса, параметр shm используется для сегментов разделяемой памяти, параметр msg - для очередей сообщений, параметр sem - для семафоров.

Если поведение программ, использующих средства System V IPC, базируется на предположении, что эти средства были созданы при их работе, не забывайте перед их запуском удалять уже существующие ресурсы.

Использование системного вызова shmctl() для освобождения ресурса

Для той же цели -- удалить область разделяемой памяти из системы -- можно воспользоваться и системным вызовом shmctK). Этот системный вызов позволяет полностью ликвидировать область разделяемой памяти в операционной системе по заданному дескриптору средства IPC, если, конечно, у вас хватает для этого полномочий. Системный вызов shmctl () позволяет выполнять и другие действия над сегментом разделяемой памяти, но их изучение лежит за пределами нашего курса.

Системный вызов shmctiO Прототип системного вызова

¦include <sys/types.h> ¦include <sys/ipc.h> ¦include <sys/shm.feint shmctl(int shmid, int and, struct shmid_ds *buf); Описание системного вызова

Системный вызов shmctl предназначен для получения информации об области разделяемой памяти, изменения ее атрибутов и удаления из системы. Данное описание не является полным описанием системного вызова, а ограничивается рамками текущего курса. Для изучения полного описания обращайтесь к UNIX Manual.

В нашем курсе мы будем пользоваться системным вызовом shmctl только для удаления области разделяемой памяти из системы. Параметр shmid является дескриптором System V IPC для сегмента разделяемой памяти, т. е. значением, которое вернул системный вызов shmget ({ при создании сегмента или при его поиске по ключу.

В качестве параметра and в рамках нашего курса мы всегда будем передавать значение IPC_KMID - команду для удаления сегмента разделяемой памяти с заданным идентификатором. Параметр bu f для этой команды не используется, поэтому мы всегда будем подставлять туда значение NULL.

Возвращаемое значение

Системный вызов возвращает значение 0 при нормальном завершении и значение -1 при возникновении ошибки.

Разделяемая память и системные вызовы fork(), ехес() и функция exit()

Важным вопросом является поведение сегментов разделяемой памяти при выполнении процессом системных вызовов fork(), exec () и функции exit ().

При выполнении системного вызова fork () все области разделяемой памяти, размещенные в адресном пространстве процесса, наследуются порожденным процессом.

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

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

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

Понятие о нити исполнения (thread) в UNIX. Идентификатор нити исполнения. Функция pthread_self ()

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

В различных версиях операционной системы UNIX существуют различные интерфейсы, обеспечивающие работу с нитями исполнения. Мы кратко ознакомимся с некоторыми функциями, позволяющими разделить процесс на thread'bi и управлять их поведением, в соответствии со стандартом POSIX. Нити исполнения, удовлетворяющие стандарту POSIX, принято называть POSIX thread'aMH или, кратко, pthread'aMH.

К сожалению, операционная система Linux не полностью поддерживает нити исполнения на уровне ядра системы. При создании нового thread'a запускается новый традиционный процесс, разделяющий с родительским традиционным процессом его ресурсы, программный код и данные, расположенные вне стека, т. е. фактически действительно создается новый thread, но ядро не умеет определять, что эти thread'bi являются составными частями одного целого. Это «знает» только специальный процесс-координатор, работающий на пользовательском уровне и стартующий при первом вызове функций, обеспечивающих POSIX интерфейс для нитей исполнения. Поэтому мы сможем наблюдать не все преимущества использования нитей исполнения (в частности, ускорить решение задачи на однопроцессорной машине с их помощью вряд ли получится), но даже в этом случае thread'bi можно задействовать как очень удобный способ для создания процессов с общими ресурсами, программным кодом и разделяемой памятью.

Каждая нить исполнения, как и процесс, имеет в системе уникальный номер -- идентификатор thread'a. Поскольку традиционный процесс в концепции нитей исполнения трактуется как процесс, содержащий единственную нить исполнения, мы можем узнать идентификатор этой нити и для любого обычного процесса. Для этого используется функция pthread_self (). Нить исполнения, создаваемую при рождении нового процесса, принято называть начальной или главной нитью исполнения этого процесса.

Функция pthread_self()

Прототип функции

#include <pthread.h> pthread_t pthread_self(void);

Описание функции

Функция pthread_self возвращает идентификатор текущей нити исполнения.

Тип данных pthread_t является синонимом для одного из целочисленных типов языка С.

Создание и завершение thread'a. Функции pthread_create(), pthread_exit(), pthreadJoin()

Нити исполнения, как и традиционные процессы, могут порождать нити-потомки, правда, только внутри своего процесса. Каждый будущий thread внутри программы должен представлять собой функцию с прототипом

void *thread(void *arg);

Параметр arg передается этой функции при создании thread'a и может, до некоторой степени, рассматриваться как аналог параметров функции main(), о которых мы говорили на семинарах 3--4. Возвращаемое функцией значение может интерпретироваться как аналог информации, которую родительский процесс может получить после завершения процесса-ребенка. Для создания новой нити исполнения применяется функция pthread_create().

Функция для создания нити исполнения Прототип функции

#include <pthread.h>

int pthread_create(pthread_t *thread, pthread_attr_t *attr, void * (*start_routine)(void *), void *arg);

Описание функции

санкция pthread_create служит для создания новой нити исполнения (thread'a) внутри текущего процесса. Настоящее описание не является полным описанием функции, а служит только целям данного курса. Для изучения полного описания обращайтесь к UNIX Manual.

Новый thread будет выполнять функцию start_routine с прототипом

void *start_routine(void *)

передавая ей в качестве аргумента параметр arg. Если требуется передать более одного параметра, они собираются в структуру, и передается адрес этой структуры. Значение, возвращаемое функцией start_routine, не должно указывать на динамический объект данного thread'a.

Параметр attr служит для задания различных атрибутов создаваемого thread'a. Их описание выходит за рамки нашего курса, и мы всегда будем считать их заданными по умолчанию, подставляя в качестве аргумента значение NULL.

Возвращаемые значения

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

При удачном завершении функция возвращает значение 0 и помещает идентификатор новой нити исполнения по адресу, на который указывает параметр thread. В случае ошибки возвращается положительное значение (а не отрицательное, как в большинстве системных вызовов и функций!), которое определяет код ошибки, описанный в файле <ermo.h>. Значение системной переменной errno при этом не устанавливается.

исполнения, например, в породившей завершившийся thread, и должен указывать на объект, не являющийся локальным для завершившегося thread'a;

* если в процессе выполняется возврат из функции main () или где-либо в процессе (в любой нити исполнения) осуществляется вызов функции exit (), это приводит к завершению Bcexthread'oB процесса.

Функция для завершения нити исполнения Прототип функции

#include <pthread.h>

void pthread_exit(void *status);

Описание функции

Функция pthread_exit служит для завершения нити исполнения (thread) текущего процесса.

Функция никогда не возвращается в вызвавший ее thread. Объект, на который указывает параметр status, может быть впоследствии изучен в другой нити исполнения, например в нити, породившей завершившуюся нить. Поэтому он не должен указывать на динамический объект завершившегося thread'a.

Одним из вариантов получения адреса, возвращаемого завершившимся thread'oM, с одновременным ожиданием его завершения является использование функции pthread_join (). Нить исполнения, вызвавшая эту функцию, переходит в состояние ожидание но завершения заданного thread'a. Функция позволяет также получить указатель, который вернул завершившийся thread в операционную систему.

Функция pthreadJoin()

Прототип функции

#include <pthread.h>

int pthread_join (pthread_t thread,

void **status_addr);

Описание функции

Функция pthreacM oin блокирует работу вызвавшей ее нити исполнения до завершения thread'a с идентификатором thread. После разблокирования в указатель, расположенный по адресу status_addr, заносится адрес, который вернул завершившийся thread либо при выходе из ассоциированной с ним функции, либо при выполнении функции pthread_exit (). Бели нас не интересует, что вернула нам нить исполнения, в качестве этого параметра можно использовать значение NULL.

Возвращаемые значения

Функция возвращает значение 0 при успешном завершении. В случае ошибки возвращается положительное значение (а не отрицательное, как в большинстве системных вызовов и функций!), которое определяет код ошибки, описанный в файле <еггпо.п>. Значение системной переменной еггпо при этом не устанавливается.

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

исполнения

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

/* Программа 06-2.с для иллюстрации работы двух нитей исполнения. Каждая нить исполнения просто увеличивает на 1 разделяемую переменную а. */ #include <pthread.h> #include <stdio.h> int a = 0 ;

/* Переменная а является глобальной статической для всей программы, поэтому она будет разделяться обеими нитями исполнения.*/

/* Ниже следует текст функции, которая будет ассоциирована со 2-м thread'ом */ void *mythread(void *dummy)

/* Параметр dummy в нашей функции не используется и присутствует только для совместимости типов данных. По той же причине функция возвращает значение void *, хотя это никак не используется в программе.*/ {

pthread_t mythid; /* Для идентификатора нити исполнения */

/* Заметим, что переменная mythid является динамической локальной переменной функции mythread(), т. е. помещается в стеке и, следовательно, не разделяется нитями исполнения. */ /* Запрашиваем идентификатор thread'а */ mythid = pthread_self(); а = а+1;

printf("Thread %d. Calculation result = %d\n",

mythid, a); return NULL;

}

/* Функция main() - она же ассоциированная функция главного thread'а */ int main() {

pthread_t thid, mythid; int result;

/* Пытаемся создать новую нить исполнения, ассоциированную с функцией mythread(). Передаем ей в качестве параметра значение NULL. В случае удачи в переменную thid занесется идентификатор нового thread'а. Если возникнет ошибка, то прекратим работу. */

result = pthread_create( &thid,

(pthread_attr_t *)NULL, mythread, NULL); if(result 1= 0){

printf ("Error on thread create, return value = %d\n", result); exit(-1); }

printf("Thread created, thid = %d\n", thid); /* Запрашиваем идентификатор главного thread'a */ mythid = pthread_self() ; a = a+1;

printf("Thread %d, Calculation result = %d\n",

mythid, a); /* Ожидаем завершения порожденного thread'a, не интересуясь, какое значение он нам вернет. Если не выполнить вызов этой функции, то возможна ситуация, когда мы завершим функцию main() до того, как выполнится порожденный thread, что автоматически повлечет за собой его завершение, исказив результаты. */

pthread_join(thid, (void **)NULL); return 0;

}

Для сборки исполняемого файла при работе редактора связей необходимо явно подключить библиотеку функций для работы с pthread'aMH, которая не подключается автоматически. Это делается с помощью добавления к команде компиляции и редактирования связей параметра -lpthread -- подключить библиотеку pthread. Наберите текст, откомпилируйте эту программу и запустите на исполнение.

Обратите внимание на отличие результатов этой программы от похожей программы, иллюстрировавшей создание нового процесса (раздел «Прогон программы с fork () с одинаковой работой родителя и ребенка»), которую мы рассматривали на семинарах 3 -- 4. Программа, создававшая новый процесс, печатала дважды одинаковые значения для переменной а, так как адресные пространства различных процессов независимы, и каждый процесс прибавлял 1 к своей собственной переменной а. Рассматриваемая программа печатает два разных значения, так как переменная а является разделяемой, и каждый thread прибавляет 1 к одной и той же переменной.

Написание, компиляция и прогон программы с использованием трех нитей исполнения

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

Необходимость синхронизации процессов и нитей исполнения, использующих общую память

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

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

Процесс 1: array[0] += 1;

Процесс 2: array[1] += 1;

Процесс 1: array[2] += 1;

Процесс 1: printf("Program 1 was spawn %d times,

program 2 - %d times, total - %d times\n", array[0], array[1], array[2]);

Тогда печать будет давать неправильные результаты. Естественно, что воспроизвести подобную последовательность действий практически нереально. Мы не сможем подобрать необходимое время старта процессов и степень загруженности вычислительной системы. Но мы можем смоделировать эту ситуацию, добавив в обе программы достаточно длительные пустые циклы перед оператором array [2 ] + = 1; Это проделано в следующих программах:

/* Программа 1 (06-За.с) для иллюстрации

некорректной работы с разделяемой памятью */

/* Мы организуем разделяемую память для массива из

трех целых чисел. Первый элемент массива является

счетчиком числа запусков программы 1, т. е. данной

программы, второй элемент массива - счетчиком числа

запусков программы 2, третий элемент массива -

счетчиком числа запусков обеих программ */

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

#include <stdio.h>

#include <errno.h>

int main()

{

int *array; /* Указатель на разделяемую память */ int shmid; /* IPC-дескриптор для области разделяемой памяти */

int new = 1; /* Флаг необходимости инициализации элементов массива */

char pathname!] = "06-За.с"; /* Имя файла,

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

key_t key; /* IPC-ключ */

long i;

/* Генерируем IPC-ключ из имени файла 06-За.с в текущей директории и номера экземпляра области разделяемой памяти 0 */ if((key = ftok(pathname,0)) < 0){ printf("Can\'t generate key\n");

exit(-1);

}

/* Пытаемся эксклюзивно создать разделяемую память для сгенерированного ключа, т. е. если для этого ключа она уже существует, системный вызов вернет отрицательное значение. Размер памяти определяем как размер массива из трех целых переменных, права доступа 0666 - чтение и запись разрешены для всех */ if((shmid = shmget(key, 3*sizeof{int),

О666 I IPC_CREATI IPC_EXCL) ) < 0){ /* В случае возникновения ошибки пытаемся определить: возникла ли она из-за того, что сегмент разделяемой памяти уже существует или по другой причине */ if(errno != EEXIST){

/* Если по другой причине - прекращаем работу */ printf{"Can\'t create shared memory\n"); exit(-1) ; } else {

/* Если из-за того, что разделяемая память уже существует - пытаемся получить ее IPC-дескриптор и, в случае удачи, сбрасываем флаг необходимости инициализации элементов массива */

if((shmid = shmget(key, 3*sizeof(int), 0)) < 0){ printf("Can\'t find shared memory\n"); exit(-1);

}

new = 0;

}

}

/* Пытаемся отобразить разделяемую память в адресное пространство текущего процесса. Обратите внимание на то, что для правильного сравнения мы явно преобразовываем значение -1 к указателю на целое.*/ if((array = (int *)shmat(shmid, NULL, 0)) == (int *)(-1)){

printf("Can't attach shared memory\n"); exit(-1);

}

/* В зависимости от значения флага new либо инициализируем массив, либо увеличиваем соответствующие счетчики */

if(new){

array [0 ] = 1;

array [1] = 0;

array[2] = 1; } else {

array[0] += 1;

for(i=0; i<1000000000L; i++);

/* Предельное значение для i может меняться в зависимости от производительности компьютера * array [2] += 1;

}

/* Печатаем новые значения счетчиков, удаляем разделяемую память из адресного пространства текущего процесса и завершаем работу */ printf("Program 1 was spawn %d times,

program 2 - %d times, total - %d times\n",

array[0], array[1], array[2]); if(shmdt(array) < 0){

printf("Can't detach shared memory\n");

exit(-1) ;

}

return 0;

}

/* Программа 2 (06-3b.c) для иллюстрации

некорректной работы с разделяемой памятью */

/* Мы организуем разделяемую память для массива

из трех целых чисел. Первый элемент массива

является счетчиком числа запусков программы 1,

т. е. данной программы, второй элемент массива -

счетчиком числа запусков программы 2, третий

элемент массива - счетчиком числа запусков обеих

программ */

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/shm.h>

#include <stdio.h>

#include <errno.h>

int main()

{

int *array; /* Указатель на разделяемую память */ int shmid; /* IPC-дескриптор для области

разделяемой памяти */ int new = 1; /* Флаг необходимости инициализации

элементов массива */ char pathname[] = "0б-3а.с"; /* Имя файла,

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

именем должен существовать в текущей директории */ key_t key; / * IPC-ключ */ long i;

/* Генерируем IPC-ключ из имени файла 0б-3а.с в текущей директории и номера экземпляра области разделяемой памяти 0 */ if((key = ftok(pathname,0)) < 0){

printf("Can\'t generate key\n");

exit ( -1) ;

}

/* Пытаемся эксклюзивно создать разделяемую память для сгенерированного ключа, т. е. если для этого ключа она уже существует, системный вызов вернет отрицательное значение. Размер памяти определяем как размер массива из трех целых переменных, права доступа Оббб - чтение и запись разрешены для всех */ if((shmid = shmget(key, 3*sizeof(int),

ОбббI IPC_CREAT!IPC_EXCL) ) < 0){ /* В случае ошибки пытаемся определить, возникла ли она из-за того, что сегмент разделяемой памяти уже существует или по другой причине * I

if(errno != EEXIST){

/* Если по другой причине - прекращаем работу */ printf("Can\'t create shared memory\n"); exit(-1);

} else {

/* Если из-за того, что разделяемая память уже существует - пытаемся получить ее IPC-дескриптор и, в случае удачи, сбрасываем флаг необходимости инициализации элементов массива */ if((shmid = shmget(key,

3*sizeof(int) , 0)) < 0) {

printf("Can\'t find shared memory\n");

exit(-1);

}

new = 0;

/* Пытаемся отобразить разделяемую память в адресное пространство текущего процесса. Обратите внимание на то, что для правильного сравнения мы явно преобразовываем значение -1 к указателю на целое.*/ if((array = (int *)shmat(shmid, NULL, 0)) == (int *) (-1)) {

printf("Can't attach shared memory\n"); exit(-1);

}

/* В зависимости от значения флага new либо инициализируем массив, либо увеличиваем соответствующие счетчики */ if(new){

array[0] = 0;

array[1] = 1;

array [2] = 1; } else {

array[1] += 1;

for(i=0; i<1000000000L; i++);

/* Предельное значение для i может меняться в зависимости от производительности компьютера */ array[2] += 1;

}

/* Печатаем новые значения счетчиков, удаляем разделяемую память из адресного пространства текущего процесса и завершаем работу */ printf("Program 1 was spawn %d times,

program 2 - %d times, total - %d times\n", array[0], array[1], array[2]); if(shmdt(array) < 0){

printf("Can't detach shared memory\n") ; exit(-1);

}

return 0;

Наберите программы, сохраните под именами 06-За.с и 06-ЗЬ.с соответственно, откомпилируйте их и запустите любую из них один раз для создания и инициализации разделяемой памяти. Затем запустите другую и, пока она находится в цикле, запустите (например, с другого виртуального терминала) снова первую программу. Вы получите неожиданный результат: количество запусков по отдельности не будет соответствовать количеству запусков вместе.

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

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

На следующем семинаре мы рассмотрим семафоры, которые являются средством System V IPC, предназначенным для синхронизации процессов.

Семинар 8. Семафоры в UNIX как средство синхронизации процессов

Семафоры в UNIX. Отличие операций над UNIX-семафорами от классических операций. Создание массива семафоров или доступ к уже существующему массиву. Системный вызов semget (). Выполнение операций над семафорами. Системный вызов semop (). Удаление набора семафоров из системы с помощью команды ipcrm или системного вызова semctl (). Понятие о POSIX-семафорах.

Ключевые слова: семафоры System V IPC, массив семафоров, операция A(S, п), операция D(S, п), операция Z(S), системные вызовы semget (), semop (), semctl (), POSIX-семафоры.

Семафоры в UNIX. Отличие операций над UNIX-семафорами от классических операций

В материалах предыдущего семинара речь шла о необходимости синхронизации работы процессов для их корректного взаимодействия через разделяемую память. Как упоминалось в лекции 6, одним из первых механизмов, предложенных для синхронизации поведения процессов, стали семафоры, концепцию которых описал Дейкстра (Dijkstra) в 1965 году. При разработке средств System V IPC семафоры вошли в их состав как неотъемлемая часть. Следует отметить, что набор операций над семафорами System V IPC отличается от классического набора операций {Р, V}, предложенного Дейкстрой. Он включает три операции:

A(S, п) -- увеличить значение семафора S на величину п;

D(S, п) -- пока значение семафора S < п, процесс блокируется. Далее

S = S - п;

* Z (S) -- процесс блокируется до тех пор, пока значение семафора S не станет равным 0.

Изначально все IPC-семафоры инициируются нулевым значением.

Мы видим, что классической операции P(S) соответствует операция D (S, 1), а классической операции V (S) соответствует операция А (S, 1). Аналогом ненулевой инициализации семафоров Дейкстры значением п может служить выполнение операции А (s, п) сразу после создания семафора S, с обеспечением атомарности создания семафора и ее выполнения посредством другого семафора. Мы показали, что классические семафоры реализуются через семафоры System V IPC. Обратное не является верным. Используя операции Р (S) и V (S), мы не сумеем реализовать операцию z (S).

Поскольку IPC-семафоры являются составной частью средств System V IPC, то для них верно все, что говорилось об этих средствах в материалах предыдущего семинара. IPC-семафоры являются средством связи с непрямой адресацией, требуют инициализации для организации взаимодействия процессов и специальных действий для освобождения системных ресурсов по его окончании. Пространством имен IPC-семафоров является множество значений ключа, генерируемых с помощью функции ftok(). Для совершения операций над семафорами системным вызовам в качестве параметра передаются IPC-дескрипторы семафоров, однозначно идентифицирующих их во всей вычислительной системе, а вся информация о семафорах располагается в адресном пространстве ядра операционной системы. Это позволяет организовывать через семафоры взаимодействие процессов, даже не находящихся в системе одновременно.

Создание массива семафоров или доступ к уже существующему. Системный вызов semget()

В целях экономии системных ресурсов операционная система UNIX позволяет создавать не по одному семафору для каждого конкретного значения ключа, а связывать с ключом целый массив семафоров (в Linux -- до 500 семафоров в массиве, хотя это количество может быть уменьшено системным администратором). Для создания массива семафоров, ассоциированного с определенным ключом, или доступа по ключу к уже существующему массиву используется системный вызов semget (), являющийся аналогом системного вызова shmget () для разделяемой памяти, который возвращает значение IPC-дескриптора для этого массива. При этом применяются те же способы создания и доступа (см. семинары 6--7 раздел «Разделяемая память в UNIX. Системные вызовы shmget(), shmat(), shmdt()»), что и для разделяемой памяти. Вновь созданные семафоры инициируются нулевым значением.

Системный вызов semgetO Прототип системного вызова

Mnclude <sys/types.h> Mnclude <sys/ipc.h> Mnclude <sys/sem.h>

int semget(key_t key, int nsems, int semflg);

Описание системного вызова

Системный вызов semget предназначен для выполнения операции доступа к массиву IPC-семафоров и, в случае ее успешного завершения, возвращает дескриптор System V IPC для этого массива (целое неотрицательное число, однозначно характеризующее массив семафоров внутри вычислительной системы и использующееся в дальнейшем для других операций с ним).

Параметр key является ключом System V IPC для массива семафоров, т. е. фактически его именем из пространства имен System V IPC. В качестве значения этого параметра может использоваться значение ключа, полученное с помощью функции f tok (), или специальное значение IPC_PRIVATE. Использование значения IPC_PRIVATE всегда приводит к попытке создания нового массива семафоров с ключом, который не совпадает со значением ключа ни одного из уже существующих массивов и не может быть получен с помощью функции f tok () ни при одной комбинации ее параметров.

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

Параметр semflg - флаги - играет роль только при создании нового массива семафоров и определяет права различных пользователей при доступе к массиву, а также необходимость создания нового массива и поведение системного вызова при попытке создания. Он является некоторой комбинацией (с помощью операции побитовое или - «I») следующих предопределенных значений и восьмеричных прав доступа:

IPC_CREAT - если массива для указанного ключа не существует, он должен быть создан;

IPC_EXCL - применяется совместно с флагом IPC_CREAT. При совместном их использовании и существовании массива с указанным ключом, доступ к массиву не производится и констатируется ошибка, при этом переменная errno, описанная в файле <errno.h>, примет значение EEXIST

0400 - разрешено чтение для пользователя, создавшего массив;

0200 - разрешена запись для пользователя, создавшего массив;

0040 - разрешено чтение для группы пользователя, создавшего массив;

0020 - разрешена запись для группы пользователя, создавшего массив;

0004 - разрешено чтение для всех остальных пользователей;

0002 - разрешена запись для всех остальных пользователей.

Вновь созданные семафоры инициируются нулевым значением.

Возвращаемое значение

Системный вызов возвращает значение дескриптора System V IPC для массива семафоров при нормальном завершении и значение -1 при возникновении ошибки.

Выполнение операций над семафорами. Системный вызов semop()

Для выполнения операций A, D и Z над семафорами из массива используется системный вызов semop (), обладающий довольно сложной семантикой. Разработчики System V IPC явно перегрузили этот вызов, применяя его не только для выполнения всех трех операций, но еще и для нескольких семафоров в массиве IPC-семафоров одновременно. Для правильного использования этого вызова необходимо выполнить следующие действия:

Определиться, для каких семафоров из массива предстоит выполнить операции. Необходимо иметь в виду, что все операции реально совершаются только перед успешным возвращением из системного вызова, т. е. если вы хотите выполнить операции A(Si,5) и Z (S2) в одном вызове и оказалось, что S2 != О, то значение семафора Si не будет изменено до тех пор, пока значение S2 не станет равным 0. Порядок выполнения операций в случае, когда процесс не переходит в состояние ожидание, не определен. Так, например, при одновременном выполнении операций A (Si, 1) hd(S2,1) в случае S2 > 1 неизвестно, что произойдет раньше -- уменьшится значение семафора S2 или увеличится значение семафора Si. Если порядок для вас важен, лучше применить несколько вызовов вместо одного.

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

Заполнить элементы массива. В поле sem_f lg каждого элемента нужно занести значение 0 (другие значения флагов в семинарах мы рассматривать не будем). В поля sem_num и sem_op следует занести номера семафоров в массиве IPC семафоров и соответствующие коды операций. Семафоры нумеруются, начиная с 0. Если у вас в массиве всего один семафор, то он будет иметь номер 0. Операции кодируются так:

для выполнения операции А (S, п) значение поля sem_op должно быть равно п;

для выполнения операции D(S,n) значение поля sem_op должно быть равно -п;

для выполнения операции Z(S) значение поля sem_op должно быть равно 0.

4. В качестве второго параметра системного вызова semop () указать ад-рес заполненного массива, а в качестве третьего параметра -- ранееопределенное количество семафоров, над которыми совершаютсяоперации.

Системный вызов semopO Прототип системного вызова

#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h>

int semop(int semid, struct sembuf *sops, int nsops); Описание системного вызова

Системный вызов semop предназначен для выполнения операций A, D и Z (см. описание операций над семафорами из массива IPC семафоров - раздел «Создание массива семафоров или доступ к уже существующему. Системный вызов semget ()»этого семинара). Данное описание не является полным описанием системного вызова, а ограничивается рамками текущего курса. Для полного описания обращайтесь к UNIX Manual.

Параметр semid является дескриптором System V IPC для набора семафоров, т. е. значением, которое вернул системный вызов semget () при создании набора семафоров или при его поиске по ключу.

Каждый из nsops элементов массива, на который указывает параметр sops, определяет операцию, которая должна быть совершена над каким-либо семафором из массива IPC семафоров, и имеет тип структуры struct sembuf, в которую входят следующие переменные:

short sem_num - номер семафора в массиве IPC-семафоров (нумеруются, начиная с 0);

short sem_op - выполняемая операция;

short sem_f lg - флаги для выполнения операции. В нашем курсе всегда будем считать эту

переменную равной 0. Значение элемента структуры sem_op определяется следующим образом:

для выполнения операции А {S, п) значение должно быть равно п;

для выполнения операции D (S, п) значение должно быть равно -п;

для выполнения операции Z (S) значение должно быть равно 0.

Семантика системного вызова подразумевает, что все операции будут в реальности выполнены над семафорами только перед успешным возвращением из системного вызова. Если при выполнении операций D или Z процесс перешел в состояние ожидания, то он может быть выведен из этого состояния при возникновении следующих форс-мажорных ситуаций:

массив семафоров был удален из системы;

процесс получил сигнал, который должен быть обработан.

В этом случае происходит возврат из системного вызова с констатацией ошибочной ситуации. Возвращаемое значение

Системный вызов возвращает значение 0 при нормальном завершении и значение -1 при возникновении ошибки.

Прогон примера с использованием семафора

Для иллюстрации сказанного рассмотрим простейшие программы, синхронизирующие свои действия с помощью семафоров:

/* Программа 08-1а.с для иллюстрации работы с семафорами */

/* Эта программа получает доступ к одному системному семафору, ждет, пока его значение не станет больше или равным 1 после запусков программы 08-1Ь.с, а затем уменьшает его на 1*/ #include <sys/types. h> #include <sys/ipc.h> #include <sys/sem.h> #include <stdio.h> int main() {

int semid; /* IPC-дескриптор для массива IPC семафоров */

char pathname[] = "08-la.c"; /* Имя файла,

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

key_t key; /* IPC ключ */

struct sembuf mybuf; /* Структура для задания

операции над семафором */ /* Генерируем IPC-ключ из имени файла 08-1а.с в текущей директории и номера экземпляра массива семафоров 0 */

if((key = ftok(pathname,0)) < 0){ printf("Can\'t generate key\n"); exit(-1);

}

/* Пытаемся получить доступ по ключу к массиву семафоров, если он существует, или создать его из одного семафора, если его еще не существует, с правами доступа read & write для всех пользователей */ if((semid = semget(key, 1, 0666 I IPC_CREAT)) < 0){

printf("Can\'t get semid\n");

exit(-1);

}

/* Выполним операцию D(semidl,l) для нашего массива семафоров. Для этого сначала заполним нашу структуру.

Флаг, как обычно, полагаем равным 0. Наш массив

семафоров состоит из одного семафора с номером 0.

Код операции -1.*/

mybu f.sem_op = -1;

mybuf.sem_flg = 0;

mybuf.sem_num = 0;

if(semop(semid, &mybuf, 1) < 0){

printf("Can\'t wait for condition\n");

exit (-1) ;

}

printf("Condition is presentXn"); return 0;

}

/* Программа 08-lb.c для иллюстрации работы с семафорами */

/* Эта программа получает доступ к одному системному

семафору и увеличивает его на 1*/

tinclude <sys/types.h>

tinclude <sys/ipc.h>

#include <sys/sem.h>

#include <stdio.h>

int main()

{

int semid; /* IPC-дескриптор для массива IPC семафоров */

char pathname[] = "08-la.c"; /* Имя файла,

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

key_t key; /* IPC ключ */

struct sembuf mybuf; /* Структура для задания операции над семафором */

/* Генерируем IPC-ключ из имени файла 08-1а.с в текущей директории и номера экземпляра массива семафоров 0 */

if((key = ftok(pathname,0)) < 0){ printf("Can\'t generate key\n"); exit(-1);

}

/* Пытаемся получить доступ по ключу к массиву семафоров, если он существует, или создать его из одного семафора, если его еще не существует, с правами доступа read & write для всех пользователей */

if((semid = semget(key, 1, 0666 1 IPC_CREAT)) < 0){ printf("Can\'t get semid\n"); exi t(-1);

}

/* Выполним операцию A(semidl,l) для нашего массива

семафоров. Для этого сначала заполним нашу структуру.

Флаг, как обычно, полагаем равным 0. Наш массив

семафоров состоит из одного семафора с номером 0.

Код операции 1.*/

mybuf.sem_op = 1;

mybuf.sem_flg = 0;

mybuf.sem_num = 0;

if(semop(semid, &mybuf, 1) < 0){

printf("Can\'t wait for condition\n");

exit(-1);

}

printf("Condition is set\n"); return 0;

}

Первая программа выполняет над семафором S операцию D (S, 1), вторая программа выполняет над тем же семафором операцию A (S, 1). Если семафора в системе не существует, любая программа создает его перед выполнением операции. Поскольку при создании семафор всегда инициируется 0 , то программа 1 может работать без блокировки только после запуска программы 2. Наберите программы, сохраните под именами 08-1а.с и 08-1Ь.с соответственно, откомпилируйте и проверьте правильность их поведения.

Изменение предыдущего примера

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

Удаление набора семафоров из системы с помощью команды ipcrm или системного вызова semctl()

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


Подобные документы

  • Основные понятия об операционных системах. Виды современных операционных систем. История развития операционных систем семейства Windows. Характеристики операционных систем семейства Windows. Новые функциональные возможности операционной системы Windows 7.

    курсовая работа [60,1 K], добавлен 18.02.2012

  • Понятие виртуальной памяти, ее реализация. Особенности страничной организации по требованию. Этапы обработки ситуации отсутствия страницы в памяти. Стратегии (алгоритмы) замещения страниц. Особенности некоторых операционных систем: Windows NT и Solaris.

    презентация [2,2 M], добавлен 24.01.2014

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

    презентация [1,3 M], добавлен 22.04.2014

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

    презентация [1,6 M], добавлен 24.01.2014

  • Характеристика сущности, назначения, функций операционных систем. Отличительные черты их эволюции. Особенности алгоритмов управления ресурсами. Современные концепции и технологии проектирования операционных систем, требования, предъявляемые к ОС XXI века.

    курсовая работа [36,4 K], добавлен 08.01.2011

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

    реферат [16,6 K], добавлен 25.02.2011

  • Важность операционной системы для мобильных устройств. Популярность операционных систем. Доля LINUX на рынке операционных систем. История OS Symbian, BlackBerry OS, Palm OS. Отличия смартфона от обычного мобильного телефона. Учет ограничений по памяти.

    презентация [477,3 K], добавлен 01.12.2015

  • Основные понятия операционных систем. Современное оборудование компьютера. Преимущества и недостатки операционной системы Linux. Функциональные возможности операционной системы Knoppix. Сравнительная характеристика операционных систем Linux и Knoppix.

    реферат [1,5 M], добавлен 17.12.2014

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

    учебное пособие [1,2 M], добавлен 24.01.2014

  • Использование операционных систем Microsoft Windows. Разработка операционной системы Windows 1.0. Возможности и характеристика последующих версий. Выпуск пользовательских операционных систем компании, доработки и нововведения, версии Windows XP и Vista.

    реферат [23,3 K], добавлен 10.01.2012

Работы в архивах красиво оформлены согласно требованиям ВУЗов и содержат рисунки, диаграммы, формулы и т.д.
PPT, PPTX и PDF-файлы представлены только в архивах.
Рекомендуем скачать работу.