С понятием атомарности при рассмотрении операций системных вызовов придется сталкиваться довольно часто. Все системные вызовы выполняются атомарно. Это означает, что ядро гарантирует завершение всех этапов системного вызова в рамках одной операции, которая не прерывается другим процессом или потоком.
Для завершения некоторых операций атомарность играет весьма важную роль. В частности, она позволяет избежать состояния гонки (которое иногда называют состязательной ситуацией). Состязательной называют ситуацию, при которой на результат, выдаваемый двумя процессами (или потоками), работающими на совместно используемых ресурсах, влияет непредсказуемость относительного порядка получения процессами доступа к центральному процессору (или процессорам).
Далее мы рассмотрим две ситуации, развивающиеся на фоне файлового ввода-вывода, при которых возникает состояние гонки. Вы увидите, как эти состязания устраняются путем использования флагов системного вызова open(), гарантирующего атомарность соответствующих файловых операций.
Мы вернемся к теме состояния гонки, когда приступим к рассмотрению системного вызова sigsuspend() в разделе 22.9 и системного вызова fork() в разделе 24.4.
Эксклюзивное создание файла
В подразделе 4.3.1 отмечалось, что указание флага O_EXCL в сочетании с флагом O_CREAT заставляет open() возвращать ошибку, если файл уже существует. Тем самым процессу гарантируется, что именно он является создателем файла. Проводимая заранее проверка существования файла и создание файла выполняются атомарно. Чтобы понять, насколько это важно, рассмотрим код, показанный в листинге 5.1. Мы могли бы им воспользоваться при отсутствии флага O_EXCL. (В этом коде выводится идентификатор процесса, возвращаемый системным вызовом getpid(), позволяющий отличить данные на выходе двух различных запусков этой программы.)
Листинг 5.1. Код, не подходящий для эксклюзивного открытия файла
Из файла fileio/bad_exclusive_open.c
fd = open(argv[1], O_WRONLY); /* Открытие 1: проверка существования файла */
if (fd!= -1) { /* Открытие прошло успешно */
printf("[PID %ld] File \"%s\" already exists\n",
(long) getpid(), argv[1]);
close(fd);
} else {
if (errno!= ENOENT) { /* Сбой по неожиданной причине */
errExit("open");
} else {
/* ОТРЕЗОК ВРЕМЕНИ НА СБОЙ */
fd = open(argv[1], O_WRONLY | O_CREAT, S_IRUSR | S_IWUSR);
if (fd == -1)
errExit("open");
printf("[PID %ld] Created file \"%s\" exclusively\n",
(long) getpid(), argv[1]); /* МОЖЕТ БЫТЬ ЛОЖЬЮ! */
}
}
Из файла fileio/bad_exclusive_open.c
Кроме пространного использования двух вызовов open(), код в листинге 5.1 содержит ошибку. Представим себе, что один из наших процессов первым вызвал open(). Файл еще не существовал, но до того, как состоялся второй вызов open(), какой-то другой процесс создал его. Это могло произойти, если диспетчер ядра решил, что отрезок времени, выделенный процессу, истек, и передал управление, как показано на рис. 5.1, другому процессу, или, если два процесса были запущены одновременно в многопроцессорной системе. На рис. 5.1 изображен случай, когда оба таких процесса выполняют код, показанный в листинге 5.1. В данном сценарии процесс А придет к неверному заключению, что файл создан именно им, поскольку второй вызов open() будет успешен независимо от того, существовал файл или нет.
Хотя шанс на заблуждение процесса относительно того, что именно он является создателем файла, относительно мал, сама возможность такого события делает этот код ненадежным. Тот факт, что исход этих операций зависит от порядка диспетчеризации двух процессов, означает, что возникло состояние гонки.
Чтобы показать несомненную проблемность кода, можно заменить закомментированную строку ОТРЕЗОК ВРЕМЕНИ НА СБОЙ в листинге 5.1 фрагментом кода, создающим искусственную задержку между проверкой существования файла и созданием файла:
Рис. 5.1. Неудачная попытка эксклюзивного создания файла
printf("[PID %ld] File \"%s\" doesn't exist yet\n", (long) getpid(), argv[1]);
if (argc > 2) { /* Задержка между проверкой и созданием */
sleep(5); /* Приостановка выполнения на 5 секунд */
printf("[PID %ld] Done sleeping\n", (long) getpid());
}
Библиотечная функция sleep() приостанавливает выполнение процесса на указанное количество секунд. Эта функция рассматривается в разделе 23.4.
Если одновременно запустить два экземпляра программы, показанной в листинге 5.1, станет видно, что они обе утверждают, что создали файл эксклюзивно:
$ ./bad_exclusive_open tfile sleep &
[PID 3317] File "tfile" doesn't exist yet