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

Все рассмотренные на этом семинаре примеры являются не совсем корректными. В большинстве случаев они работают правильно, однако возможны ситуации, когда совместная деятельность этих процессов или нитей исполнения приводит к неверным и неожиданным результатам. Это связано с тем, что любые неатомарные операции, связанные с изменением содержимого разделяемой памяти, представляют собой критическую секцию процесса или нити исполнения. Вспомните рассмотрение критических секций в лекции 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-3а.с) для иллюстрации

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

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

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

запусков программы 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-3a.c"; /* Имя файла,

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

именем должен существовать в текущей директории */

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

long i;

/* Генерируем IPC ключ из имени файла 06-3a.c в

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

разделяемой памяти 0 */

if((key = ftok(pathname,0)) < 0){

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

exit(-1);

}

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

сгенерированного ключа, т.е. если для этого ключа она

уже существует, системный вызов вернет отрицательное

значение. Размер памяти определяем как размер массива

из 3-х целых переменных, права доступа 0666 – чтение и

запись разрешены для всех */

if((shmid = shmget(key, 3*sizeof(int),

0666|IPC_CREAT|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;

}

Листинг 6.3a. Программа 1 (06-3а.с) для иллюстрации некорректной работы с разделяемой памятью.

/* Программа 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[] = "06-3a.c"; /* Имя файла,

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

именем должен существовать в текущей директории */

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

long i;

/* Генерируем IPC ключ из имени файла 06-3a.c в текущей

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

памяти 0 */

if((key = ftok(pathname,0)) < 0){

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

exit(-1);

}

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

сгенерированного ключа, т.е. если для этого ключа она

уже существует, системный вызов вернет отрицательное

значение. Размер памяти определяем как размер массива

из трех целых переменных, права доступа 0666 – чтение

и запись разрешены для всех */

if((shmid = shmget(key, 3*sizeof(int),

0666|IPC_CREAT|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] = 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;

}

Листинг 6.3b. Программа 2 (06-3b.c) для иллюстрации некорректной работы с разделяемой памятью.

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

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

Наши рекомендации