Московский физико-технический институт
Опубликован: 16.09.2004 | Доступ: свободный | Студентов: 7773 / 1592 | Оценка: 4.44 / 4.28 | Длительность: 15:33:00
Лекция 4:

Средства System V IPC. Организация работы с разделяемой памятью в UNIX. Понятие нитей исполнения (thread)

< Лекция 3 || Лекция 4: 12345 || Лекция 5 >

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

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

/* Программа 06-2.c для иллюстрации работы двух нитей исполнения.
Каждая нить исполнения просто увеличивает на 1 разделяемую 
переменную a. */ 
#include <pthread.h>
#include <stdio.h>
int a = 0; 
/* Переменная a является глобальной статической для всей 
программы, поэтому она будет разделяться обеими нитями 
исполнения.*/
/* Ниже следует текст функции, которая будет 
ассоциирована со 2-м thread'ом */
void *mythread(void *dummy)
/* Параметр dummy в нашей функции не используется и 
присутствует только для совместимости типов данных. По той
же причине функция возвращает значение void *, хотя
это никак не используется в программе.*/
{
    pthread_t mythid; /* Для идентификатора нити исполнения */
    /* Заметим, что переменная mythid является 
динамической локальной переменной функции mythread(), 
т. е. помещается в стеке и, следовательно, не разделяется
нитями исполнения. */
    /* Запрашиваем идентификатор thread'а */
    mythid = pthread_self();
    a = a+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 != 0){
    printf ("Error on thread create, 
        return value = %d\n", result);
    exit(-1);
    }
    printf("Thread created, thid = %d\n", thid);
    /* Запрашиваем идентификатор главного thread'а */
    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;
}
Листинг 6.2. Программа 06-2.c для иллюстрации работы двух нитей исполнения.

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

Обратите внимание на отличие результатов этой программы от похожей программы, иллюстрировавшей создание нового процесса (раздел "Прогон программы с fork() с одинаковой работой родителя и ребенка"), которую мы рассматривали на семинарах 3-4. Программа, создававшая новый процесс, печатала дважды одинаковые значения для переменной a, так как адресные пространства различных процессов независимы, и каждый процесс прибавлял 1 к своей собственной переменной a . Рассматриваемая программа печатает два разных значения, так как переменная a является разделяемой, и каждый 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-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 алгоритмов синхронизации, например, алгоритма Петерсона или алгоритма булочной.

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

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

< Лекция 3 || Лекция 4: 12345 || Лекция 5 >
лия логовина
лия логовина

организовать двустороннюю поочередную связь процесса-родителя и процесса-ребенка через pipe, используя для синхронизации сигналы sigusr1 и sigusr2.

Макар Оганесов
Макар Оганесов