Книга: UNIX: взаимодействие процессов
9.1. Введение
Разделы на этой странице:
9.1. Введение
Блокировки чтения-записи, описанные в предыдущей главе, представляют собой хранящиеся в памяти переменные типа pthread_rwlock_t. Эти переменные могут использоваться потоками одного процесса (этот режим работы установлен по умолчанию) либо несколькими процессами при условии, что переменные располагаются в разделяемой этими процессами памяти и при их инициализации был установлен атрибут PTHREAD_PROCESS_SHARED,
В этой главе описан усовершенствованный тип блокировки чтения-записи, который может использоваться родственными и неродственными процессами при совместном доступе к файлу. Обращение к блокируемому файлу осуществляется через его дескриптор, а функция для работы с блокировкой называется fcntl. Такой тип блокировки обычно хранится в ядре, причем информация о владельце блокировки хранится в виде его идентификатора процесса. Таким образом, блокировки записей fcntl могут использоваться только несколькими процессами, но не отдельными потоками одного процесса.
В этой главе мы в первый раз встретимся с нашим примером на увеличение последовательного номера. Рассмотрим следующую ситуацию, с которой столкнулись, например, разработчики спулера печати для Unix (команда lpr в BSD и lp в System V). Процесс, помещающий задания в очередь печати для последующей их обработки другим процессом, должен присваивать каждому из них уникальный последовательный номер. Идентификатор процесса, уникальный во время его выполнения, не может использоваться как последовательный номер, поскольку задание может просуществовать достаточно долго для того, чтобы этот идентификатор был повторно использован другим процессом. Процесс может также отправить на печать несколько заданий, каждому из которых нужно будет присвоить уникальный номер. Метод, используемый спулерами печати, заключается в том, чтобы хранить очередной порядковый номер задания для каждого принтера в отдельном файле. Этот файл содержит всего одну строку с порядковым номером в формате ASCII. Каждый процесс, которому нужно воспользоваться этим номером, должен выполнить следующие три действия:
1. Считать порядковый номер из файла.
2. Использовать этот номер.
3. Увеличить его на единицу и записать обратно в файл.
Проблема в том, что пока один процесс выполняет эти три действия, другой процесс может параллельно делать то же самое. В итоге возникнет полный беспорядок с номерами, как мы увидим в следующих примерах.
ПРИМЕЧАНИЕ
Описанная выше проблема называется проблемой взаимных исключений. Она может быть решена с использованием взаимных исключений из главы 7 или блокировок чтения-записи из главы 8. Различие состоит в том, что здесь мы предполагаем неродственность процессов, что усложняет использование предложенных выше методов. Мы могли бы использовать разделяемую память (подробно об этом говорится в четвертой части книги), поместив в нее переменную синхронизации одного из этих типов, но для неродственных процессов проще воспользоваться блокировкой fcntl. Другим фактором в данном случае стало то, что проблема со спулерами печати возникла задолго до появления взаимных исключений, условных переменных и блокировок чтения-записи. Блокировка записей была добавлена в Unix в начале 80-х, до того как появились концепции разделяемой памяти и программных потоков.
Таким образом, процессу нужно заблокировать файл, чтобы никакой другой процесс не мог получить к нему доступ, пока первый выполняет свои три действия. В листинге 9.2 приведен текст простой программы, выполняющей соответствующие действия. Функции my_lock и my_unlock обеспечивают блокирование и разблокирование файла в соответствующие моменты. Мы приведем несколько возможных вариантов реализации этих функций.
20 Каждый раз при прохождении цикла мы выводим имя программы (argv[0]) перед порядковым номером, поскольку эта функция main будет использоваться с различными версиями функций блокировки и нам бы хотелось видеть, какая версия программы выводит данную последовательность порядковых номеров.
ПРИМЕЧАНИЕ
Вывод идентификатора процесса требует преобразования переменной типа pid_t к типу long и последующего использования строки формата %ld. Проблема тут в том, что идентификатор процесса принадлежит к одному из целых типов, но мы не знаем, к какому именно, поэтому предполагается наиболее вместительный — long. Если бы мы предположили, что идентификатор имеет тип int и использовали бы строку %d, a pid_t на самом деле являлся бы типом long, код мог бы работать неправильно.
Посмотрим, что будет, если не использовать блокировку. В листинге 9.1[1] приведены версии функций my_lock и my_unlock, которые вообще ничего не делают.
Листинг 9.1. Функции, не осуществляющие блокировку
//lock/locknone.c
1 void
2 my_lock(int fd)
3 {
4 return;
5 }
6 void
7 my_unlock(int fd)
8 {
9 return;
10 }
Листинг 9.2. Функция main для примеров с блокировкой файла
//lock/lockmain.c
1 #include "unpipc.h"
2 #define SEQFILE "seqno" /* имя файла */
3 void my_lock(int), my_unlock(int);
4 int
5 main(int argc, char **argv)
6 {
7 int fd;
8 long i, seqno;
9 pid_t pid;
10 ssize_t n;
11 char line[MAXLINE + 1];
12 pid = getpid();
13 fd = Open(SEQFILE, O_RDWR, FILE_MODE);
14 for (i = 0; i < 20; i++) {
15 my_lock(fd); /* блокируем файл */
16 Lseek(fd, 0L, SEEK_SET); /* переходим к его началу */
17 n = Read(fd, line, MAXLINE);
18 line[n] = ''; /* завершающий 0 для sscanf */
19 n = sscanf(line, "%ldn", &seqno);
20 printf(%s; pid = %ld, seq# = %ldn", argv[0], (long) pid, seqno);
21 seqno++; /* увеличиваем порядковый номер */
22 snprintf(line, sizeof(line), "%ldn", seqno);
23 Lseek(fd, 0L, SEEK_SET); /* переходим на начало перед записью */
24 Write(fd, line, strlen(line));
25 my_unlock(fd); /* разблокируем файл */
26 }
27 exit(0);
28 }
Если начальное значение порядкового номера в файле было 1 и был запущен только один экземпляр программы, мы увидим следующий результат:
solaris % locknone
locknone: pid = 15491, seq# = 1
locknone: pid = 15491, seq# = 2
locknone: pid = 15491, seq# = 3
locknone: pid = 15491, seq# = 4
locknone: pid = 15491. seq# = 5
locknone: pid = 15491, seq# = 6
locknone: pid = 15491, seq# = 7
locknone: pid = 15491, seq# – 8
locknone: pid = 15491, seq# = 9
locknone: pid = 15491, seq# = 10
locknone: pid = 15491, seq# = 11
locknone: pid = 15491, seq# = 12
locknone: pid = 15491, seq# = 13
locknone: pid = 15491, seq# = 14
locknone: pid = 15491, seq# = 15
locknone: pid = 15491, seq# = 16
locknone: pid = 15491, seq# = 17
locknone: pid = 15491, seq# = 18
locknone: pid = 15491, seq# = 19
locknone: pid = 15491, seq# = 20
ПРИМЕЧАНИЕ
Обратите внимание, что функция main хранится в файле lockmain.c, но мы компилируем и компонуем эту программу с функциями, не осуществляющими никакой блокировки (листинг 9.1), поэтому мы называем ее locknone. Ниже будут использоваться другие версии функций my_lock и my_unlock, и исполняемый файл будет называться по-другому в соответствии с используемым методом блокировки.
Установим значение последовательного номера в файле обратно в единицу и запустим программу в двух экземплярах в фоновом режиме. Результат будет такой:
solaris % locknone & locknone&
solaris % locknone: pid = 15498, seq# = 1
locknone: pid = 15498, seq# = 2
locknone: pid = 15498, seq# = 3
locknone: pid = 15498, seq# = 4
locknone: pid = 15498, seq# = 5
locknone: pid = 15498, seq# = 6
locknone: pid = 15498, seq# = 7
locknone: pid = 15498, seq# = 8
locknone: pid = 15498, seq# = 9
locknone: pid = 15498, seq# = 10
locknone: pid = 15498, seq# = 11
locknone: pid = 15498, seq# = 12
locknone: pid = 15498, seq# = 13
locknone: pid = 15498, seq# = 14
locknone: pid = 15498, seq# = 15
locknone: pid = 15498, seq# = 16
locknone: pid = 15498, seq# = 17
locknone: pid = 15498, seq# = 18
locknone: pid = 15498, seq# = 19
locknone: pid = 15498, seq# = 20
locknone: pid = 15499, seq# = 1
locknone: pid = 15499, seq# = 2
locknone: pid = 15499, seq# = 3
locknone: pid = 15499, seq# = 4
locknone: pid = 15499, seq# = 5
locknone: pid = 15499, seq# = 6
locknone: pid = 15499, seq# = 7
locknone: pid = 15499, seq# = 8
locknone: pid = 15499, seq# = 9
locknone: pid – 15499, seq# = 10
locknone: pid = 15499, seq# = 11
locknone: pid = 15499, seq# – 12
locknone: pid = 15499, seq# = 13
locknone: pid = 15499, seq# = 14
locknone: pid = 15499, seq# = 15
locknone: pid = 15499, seq# = 16
locknone: pid = 15499, seq# = 17
locknone: pid = 15499, seq# = 18
locknone: pid = 15499, seq# = 19
locknone: pid = 15499, seq# = 20
Первое, на что мы обращаем внимание, — подсказка интерпретатора, появившаяся до начала текста, выводимого программой. Это нормально и всегда имеет место при запуске программ в фоновом режиме.
Первые двадцать строк вывода не содержат ошибок. Они были сформированы первым экземпляром программы (с идентификатором 15 498). Проблема возникает в первой строке, выведенной вторым экземпляром (идентификатор 15499): он напечатал порядковый номер 1. Получилось это, скорее всего, так: второй процесс был запущен ядром, считал из файла порядковый номер (1), а затем управление было передано первому процессу, который работал до завершения. Затем второй процесс снова получил управление и продолжил выполняться с тем значением порядкового номера, которое было им уже считано (1). Это не то, что нам нужно. Каждый процесс считывает значение, увеличивает его и записывает обратно 20 раз (на экран выведено ровно 40 строк), поэтому конечное значение номера должно быть 40.
Нам нужно каким-то образом предотвратить изменение файла с порядковым номером на протяжении выполнения трех действий одним из процессов. Эти действия должны выполняться как атомарная операция по отношению к другим процессам. Код между вызовами my_lock и my_unlock представляет собой критическую область (глава 7).
При запуске двух экземпляров программы в фоновом режиме результат на самом деле непредсказуем. Нет никакой гарантии, что при каждом запуске мы будем получать один и тот же результат. Это нормально, если три действия будут выполняться как одна атомарная операция; в этом случае конечное значение порядкового номера все равно будет 40. Однако при неатомарном выполнении конечное значение часто будет отличным от 40, и это нас не устраивает. Например, нам безразлично, будет ли порядковый номер увеличен от 1 до 20 первым процессом и от 21 до 40 вторым или же процессы будут по очереди увеличивать его значение на единицу. Неопределенность не делает результат неправильным, а вот атомарность выполнения операций — делает. Однако неопределенность выполнения усложняет отладку программ.
- 9.1. Введение
- 9.2. Блокирование записей и файлов
- 9.3. Блокирование записей с помощью fcntl по стандарту Posix
- 9.4. Рекомендательная блокировка
- 9.5. Обязательная блокировка
- 9.6. Приоритет чтения и записи
- 9.7. Запуск единственного экземпляра демона
- 9.8. Блокирование файлов
- 9.9. Блокирование в NFS
- 9.10. Резюме
- Упражнения