[APUE]Разделяют ли родительский и дочерний файлы одно и то же смещение файла после разветвления?

В разделе 8.3 fork function APUE о совместном использовании файлов между родительским и дочерним процессами
сказано: It is important that the parent and the child share the same file offset.

А в разделе 8.9 Race Conditions есть пример: и родитель, и дочерний элемент пишут
в файл, который открывается перед вызовом функции fork. Программа содержит состояние гонки,
потому что вывод зависит от порядка, в котором процессы выполняются ядром, и от того, как долго выполняется каждый процесс.

Но в моем тестовом коде выходные данные перекрываются.

[Langzi@Freedom apue]$ cat race.out
это длинный длинный выводэто длинный длинный вывод от родителя

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

Есть ли ошибка в моем коде? Или я неправильно понял смысл совместного использования смещения?
Будем признательны за любые советы и помощь.

следующий мой код:

#include "apue.h"
#include <fcntl.h>

void charatatime(int fd, char *);

int main()
{
 pid_t pid;
 int fd;
 if ((fd = open("race.out", (O_WRONLY | O_CREAT |  O_TRUNC),
     S_IRUSR | S_IWUSR)) < 0)
  err_sys("open error");

 if ((pid = fork()) < 0)
  err_sys("fork error");
 else if (pid == 0)
  charatatime(fd, "this is a long long output from child\n");
 else
  charatatime(fd, "this is a long long output from parent\n");

 exit(0);
}


void charatatime(int fd, char *str)
{
 // try to make the two processes switch as often as possible
 // to demonstrate the race condition.
 // set synchronous flag for fd
 set_fl(fd, O_SYNC);
 while (*str) {
  write(fd, str++, 1);
  // make sure the data is write to disk
  fdatasync(fd);
 }
}

person OnTheEasiestWay    schedule 28.10.2009    source источник


Ответы (5)


Ну, я был неправ.

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

this is a long long output from chredt

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

Итак, они делят смещение.

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

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

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

Но в итоге вы получаете количество байтов в файле, которое находится где-то посередине, и это означает, что смещения являются общими и не обновляются атомарно, и это просто странно. Но так, видимо, и происходит. Как странно.

  1. процесс A считывает смещение в A.offset
  2. процесс B считывает смещение в B.offset
  3. процесс A записывает байт в A.offset
  4. процесс A устанавливает смещение = A.offset + 1
  5. процесс B записывает байт в B.offset
  6. процесс A считывает смещение в A.offset
  7. процесс B устанавливает смещение = B.offset + 1
  8. процесс A записывает байт в A.offset
  9. процесс A устанавливает смещение = A.offset + 1
  10. процесс B считывает смещение в B.offset
  11. процесс B записывает байт в B.offset
  12. процесс B устанавливает смещение = B.offset + 1

Примерно такой должна быть последовательность событий. Как очень странно.

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

person Omnifarious    schedule 28.10.2009
comment
Спасибо за ответ, теперь мне кажется все ясно. Смещение должно быть общим, так как размер файла больше, чем длинная строка, которую два процесса записывают в файл. И я попытался использовать флаг O_APPEND, чтобы открыть файл, и результат такой, как и ожидалось. Отличие заключается в том, что флаг O_APPEND выполняет следующие два шага как атомарную операцию: 1. устанавливает смещение на новый размер файла, даже если размер файла изменяется. 2. записать в файл. Поскольку в двух процессах нет гонки, значит, результат правильный. - person OnTheEasiestWay; 28.10.2009
comment
Но с флагом O_APPEND результат должен быть правильным для любых двух процессов, которые пишут в один и тот же файл, независимо от того, являются ли эти два процесса родительскими и дочерними отношениями или нет. Если два процесса открывают файл дважды, результат также будет правильным. Флаг O_APPEND похож на системные вызовы pread и pwrite, все они являются атомарными операциями. Поскольку существует состояние гонки, мы должны использовать некоторую форму сигнализации, как сказал APUE. - person OnTheEasiestWay; 28.10.2009
comment
O_APPEND больше похож на атомарное выполнение lseek(fd, 0, SEEK_END) и запись каждый раз, когда вы записываете в файл. - person Omnifarious; 28.10.2009

Родительский и дочерний элементы совместно используют одну и ту же запись таблицы файлов в ядре, которая включает смещение. Таким образом, родительский и дочерний процессы не могут иметь разные смещения без закрытия и повторного открытия файла одним или обоими процессами. Таким образом, любая запись родителем использует это смещение и изменяет (увеличивает) смещение. Затем любая запись дочерним элементом использует новое смещение и изменяет его. Написание одного символа за раз усугубляет эту ситуацию.

Из моей справочной страницы write(2): «Настройка смещения файла и операция записи выполняются как атомарный шаг».

Таким образом, вы можете быть уверены, что ни одна запись от одного (родительского или дочернего) не будет писать поверх другого. Вы также можете заметить, что если бы вы написали (2) все предложение сразу (в одном вызове write(2)), то гарантировано, что предложение будет написано вместе, как одна часть.

На практике многие системы записывают файлы журналов таким образом. Многие связанные процессы (потомки одного и того же родителя) будут иметь файловый дескриптор, который был открыт родителем. Пока каждый из них записывает всю строку за раз (с одним вызовом write(2)), файл журнала будет читаться так, как вы этого хотите. Написание персонажа за раз не будет иметь таких же гарантий. Использование буферизации вывода (скажем, с помощью stdio) также снимет гарантии.

person Rob F    schedule 28.10.2009
comment
Я думаю, что следующее верно только для файла, открытого с флагом O_APPEND, ›Из моей справочной страницы write(2): Настройка смещения файла и ›операция записи выполняются как атомарный шаг. Как видно из вывода, нет гарантии, что запись является атомарной операцией. - person OnTheEasiestWay; 29.10.2009
comment
То, что я процитировал из write(2), верно независимо от того, истинно ли O_APPEND или нет, пока файл открыт для записи. Сама запись и регулировка смещения файла выполняются атомарно, поэтому, если запись происходила в конец файла, смещение настраивается на новый конец файла, и все это за одну операцию. O_APPEND необходим только в том случае, если файл открывается независимо более чем одним процессом, и в этом случае вызывает 2 корректировки смещения файла при каждой записи - один перед самой записью и один после, и все атомарно. - person Rob F; 31.10.2009
comment
Исходный вопрос описывал open(2) перед fork(2), и в этом случае смещение файла совместно используется родителем и дочерним элементом (а также любыми последующими дочерними элементами любого из них. Как только какой-либо конкретный процесс в семье закрывает файл, он больше не будет передавать его своим потомкам, хотя это не повлияет на его статус ни в одном из других процессов. - person Rob F; 31.10.2009
comment
Также в исходном вопросе символы записывались по одному, то есть для каждого написанного символа был отдельный вызов write(2). Нельзя ничего сказать об атомарности вызова write(2), когда записывается только один символ. Если бы весь буфер/строка был записан одним вызовом write(2) в каждом процессе, атомарность вызова write(2) была бы очевидной. - person Rob F; 31.10.2009
comment
Из того, что вы сказали, а) файл открывается для записи, б) смещение файла совместно используется родителем и дочерним элементом, в) сама запись и настройка смещения файла выполняются атомарно. Почему конечный результат перекрывается? Возможно, вы пропустили несколько слов в своем последнем комментарии о том, что у каждого символа есть собственный вызов записи, поэтому я не могу правильно понять ваш смысл. Это атомарная операция по записи символа с вызовом записи? Или только запись всего буфера/строки с вызовом записи является атомарной? - person OnTheEasiestWay; 01.11.2009

Что ж, я настроил код для компиляции на vanilla GCC/glibc, и вот пример вывода:

thhis isias a l long oulout futput frd
 parent

И я думаю, что это поддерживает идею о том, что позиция в файле является общей и может участвовать в гонках, вот почему это так странно. Обратите внимание, что данные, которые я показал, содержат 47 символов. Это больше, чем 38 или 39 символов любого отдельного сообщения, и меньше, чем 77 символов обоих сообщений вместе — единственный способ, которым я могу видеть это, — это если процессы иногда соревнуются, чтобы обновить позицию в файле — каждый из них записывает каждый из них пытается увеличить позицию, но из-за расы происходит только одно увеличение, а некоторые символы перезаписываются.

Подтверждающие доказательства: man 2 lseek в моей системе ясно сказано

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

person hobbs    schedule 28.10.2009
comment
Спасибо за Ваш ответ. Это позволяет мне ясно понять проблему. - person OnTheEasiestWay; 28.10.2009

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

person David Brown    schedule 28.10.2009
comment
На странице руководства по открытию указано, что смещение файла и флаги состояния файла хранятся в «описании открытого файла». Это описание открытого файла совпадает с таблицей открытых файлов, которую вы упомянули выше? - person OnTheEasiestWay; 28.10.2009
comment
Я считаю, что это так. В UNIX каждый процесс хранит свою собственную таблицу файловых дескрипторов, а ядро ​​имеет свою собственную таблицу открытых файлов (и такие вещи, как подсчет количества ссылок на этот файл). Итак, я предполагаю, что это зависит от реализации, хранит ли ОС смещение в таблице открытых файлов процессора или в ядре. Возможно, в исходном стандарте UNIX было так, как говорит APUE, в то время как в более современных реализациях каждому процессу присваивается собственное смещение. - person David Brown; 28.10.2009
comment
Нет, в современных реализациях смещение остается в таблице файлов ядра. - person Omnifarious; 28.10.2009
comment
Современные реализации, оригинальная реализация и каждая правильная реализация. Многое в Unix зависит от глобальных fds. Это FILE * уровня libc, которые являются локальными для процесса, но они просто проксируют файловые дескрипторы. - person hobbs; 28.10.2009

используйте pwrite, так как запись когда-нибудь заканчивается состоянием гонки, когда один и тот же ресурс (write()) используется несколькими процессами, поскольку запись не оставляет файл pos = 0 после завершения, например, вы оказываетесь в середине файла, поэтому указатель файла (fd) указывая на это место, и если другой процесс хочет что-то сделать, он производит или работает не так, как хотел, поскольку дескриптор файла будет общим для разветвления !!

Попробуйте и дайте мне обратную связь

person Manish    schedule 27.10.2010