Я попытался диагностировать ошибку в приложении, написанном на C для Linux. Оказалось, что ошибка была вызвана забыванием fclose
в дочернем процессе, когда дескриптор FILE *
все еще открыт в родительском процессе.
Файловая операция только read
. Нет операции записи.
Приложение работает на Linux 5.4.0-58-generic
. В данном случае произошла ошибка.
Приложение работает на Linux 5.10.0-051000-generic
. В данном случае бага нет, чего я и ожидал.
Родительский процесс выполняет случайное число системных вызовов fork
, если в дочернем процессе нет fclose
.
Я прекрасно понимаю, что забывание fclose
приведет к утечке памяти, но:
exit(3)
не _exit(2)
.fclose
в дочернем процессе влияет на родительский процесс?Это ошибка ядра Linux, исправленная в версии после 5.4
. Пока у меня нет доказательств, но мой тест показал мне это.
Я смог исправить эту ошибку приложения, вызвав fclose
в дочернем процессе перед его выходом. Но я хочу знать, что на самом деле происходит в этом случае. Итак, мой вопрос: почему забывание fclose
в дочернем процессе влияет на родительский процесс?
fclose
в дочернем процессе. test2.c не вызывает fclose
в дочернем процессе.123123123
123123123
123123123
123123123
123123123
123123123
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#define TICK do { putchar('.'); fflush(stdout); } while(0)
int main() {
char buff[1024] = {0};
FILE *handle = fopen("test.txt", "r");
uint32_t num_of_forks = 0;
while (fgets(buff, 1024, handle) != NULL) {
TICK;
num_of_forks++;
pid_t pid = fork();
if (pid == -1) {
printf("Fork error: %s\n", strerror(errno));
continue;
}
if (pid == 0) {
fclose(handle);
exit(0);
}
}
fclose(handle);
putchar('\n');
printf("Number of forks: %d\n", num_of_forks);
wait(NULL);
}
#include <stdio.h>
#include <errno.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
#define TICK do { putchar('.'); fflush(stdout); } while(0)
int main() {
char buff[1024] = {0};
FILE *handle = fopen("test.txt", "r");
uint32_t num_of_forks = 0;
while (fgets(buff, 1024, handle) != NULL) {
TICK;
num_of_forks++;
pid_t pid = fork();
if (pid == -1) {
printf("Fork error: %s\n", strerror(errno));
continue;
}
if (pid == 0) {
// fclose(handle);
exit(0);
}
}
fclose(handle);
putchar('\n');
printf("Number of forks: %d\n", num_of_forks);
wait(NULL);
}
ammarfaizi2@integral:/tmp$ uname -r
5.4.0-58-generic
ammarfaizi2@integral:/tmp$ gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
ammarfaizi2@integral:/tmp$ ldd --version
ldd (Ubuntu GLIBC 2.31-0ubuntu9.1) 2.31
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
ammarfaizi2@integral:/tmp$ cat test.txt
123123123
123123123
123123123
123123123
123123123
123123123
ammarfaizi2@integral:/tmp$ diff test1.c test2.c
27c27
< fclose(handle);
---
> // fclose(handle);
ammarfaizi2@integral:/tmp$ gcc test1.c -o test1 && gcc test2.c -o test2
ammarfaizi2@integral:/tmp$ ./test1
......
Number of forks: 6
ammarfaizi2@integral:/tmp$ ./test1
......
Number of forks: 6
ammarfaizi2@integral:/tmp$ ./test1
......
Number of forks: 6
ammarfaizi2@integral:/tmp$ ./test2
..................................................................................................................................................................................
Number of forks: 178
ammarfaizi2@integral:/tmp$ ./test2
............................................................................................................................................................................................................................................................................................................................................................
Number of forks: 348
ammarfaizi2@integral:/tmp$ ./test2
...........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
Number of forks: 475
ammarfaizi2@integral:/tmp$ md5sum test1 test2
c32d03916b9b72546b966223837fd115 test1
f314d2135092362288a66f53b37ffa4d test2
root@esteh:/tmp# uname -r
5.10.0-051000-generic
root@esteh:/tmp# gcc --version
gcc (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0
Copyright (C) 2019 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
root@esteh:/tmp# ldd --version
ldd (Ubuntu GLIBC 2.31-0ubuntu9.1) 2.31
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
Written by Roland McGrath and Ulrich Drepper.
root@esteh:/tmp# cat test.txt
123123123
123123123
123123123
123123123
123123123
123123123
root@esteh:/tmp# diff test1.c test2.c
27c27
< fclose(handle);
---
> // fclose(handle);
root@esteh:/tmp# gcc test1.c -o test1 && gcc test2.c -o test2
root@esteh:/tmp# ./test1
......
Number of forks: 6
root@esteh:/tmp# ./test1
......
Number of forks: 6
root@esteh:/tmp# ./test1
......
Number of forks: 6
root@esteh:/tmp# ./test2
......
Number of forks: 6
root@esteh:/tmp# ./test2
......
Number of forks: 6
root@esteh:/tmp# ./test2
......
Number of forks: 6
root@esteh:/tmp# md5sum test1 test2 # Make sure the files are identical with case 1
c32d03916b9b72546b966223837fd115 test1
f314d2135092362288a66f53b37ffa4d test2
fclose
в дочернем процессе на Linux 5.4.0-58-generic
, системный вызов fork в родительском процессе будет странным.Linux 5.10.0-051000-generic
.@iBug спасибо за исправление. Я хочу кое-что прояснить, чтобы убедиться, что я понимаю ваш комментарий на более высоком уровне (вид из utils в man 3 xxxx
). -- Итак, если я открываю файл с помощью fopen(3)
, затем вызываю fork(2)
, затем изменяю смещение file handle which is created by the parent process
с помощью fseek(3)
от дочернего процесса, тогда смещение дескриптора файла в родительском будет меняться в соответствии с вызовом fseek(3)
, который я делаю в дочернем процессе. . Это правильно?
Да, точно. Но имейте в виду, что планирование процесса может сделать результат непредсказуемым, если вы не реализуете какую-то «синхронизацию».
Обеспечьте версию времени выполнения glibc в каждой системе.
@HadiBrais хорошо, я только что предоставил версию glibc для каждой системы (пост тоже был отредактирован). Обе версии идентичны ldd (Ubuntu GLIBC 2.31-0ubuntu9.1) 2.31
. Я проверил это командой ldd --version
.
Я думаю, что ваша проблема может быть тесно связана с проблемой, проанализированной в Почему разветвление моего процесса приводит к бесконечному чтению файла?
Благодаря @JonathanLeffler я смог обнаружить реальную проблему из вашего ответа в этой теме.
Спасибо @iBug за понимание планирования.
Я подозревал, что у вас другая версия glibc в системе Linux 5.10, в которой код очистки ввода-вывода отличается. Поэтому и спросил версии. Но теперь мы знаем, что «странное» поведение происходит в обеих системах, так что это уже не имеет значения. Кстати, ldd --version
дает вам версию компоновщика, а не версию времени выполнения glibc, которую можно получить, вызвав gnu_get_libc_version()
.
Спасибо @Jonathan Leffler!
Эта проблема является дубликатом Почему разветвление моего процесса приводит к бесконечному чтению файла
Недостающее знание, почему ошибка не возникает на Linux 5.10.0-051000-generic
оказалось, что она не связана с ядром.
Оказалось, что родительский процесс конкурирует с дочерними процессами (не связанными с ядром).
fclose(3)
, дочерние процессы будут вызывать lseek(2)
, как только они вызовут exit(3)
. Это приведет к тому, что родитель повторно прочитает одно и то же смещение, потому что дочерние элементы вызывают lseek(2)
с отрицательным смещением + SEEK_CUR
.(Я не знаю, почему необходимо вызывать lseek(2)
перед выходом, возможно, это было объяснено в ответе @Jonathan Leffler, я не читал внимательно весь ответ).
lseek(2)
. Тогда вообще нет проблем.Кроме того, как уже упоминал @iBug. Но имейте в виду, что планирование процесса может сделать результат непредсказуемым, если вы не реализуете какую-то «синхронизацию».
Родительский процесс на машине Linux 5.10.0-051000-generic
, которую я использовал, был просто удачным процессом, который всегда выигрывал, чтобы сначала прочитать весь файл, прежде чем дочерний вызов lseek(2)
.
Я попытался добавить больше строк в файл (до 150 строк), поэтому родитель в основном будет медленнее, чем чтение 6 строк, и произойдет неопределенное поведение.
Результат теста: https://gist.githubusercontent.com/ammarfaizi2/b72bd03fcc13779f96b8bbeef9253e66/raw/da1eff4ed5434aa51929e5c810d54de8ffe15548/test2_fix.txt
Почему происходит lseek: это неудачная часть fflush
, которая нужна в общем случае fflush, но, вероятно, не при выходе. (Если только придерживаться буквы некоторых спецификаций не требует, например, если exit(3)
указывает fflush(3)
по имени.) И да, я нашел это внизу отличного ответа Джонатана. Обходной путь заключается в fflush(stdin)
перед разветвлением, поэтому выход обнаружит, что буфер stdin пуст (и, таким образом, позиция fd Unix синхронизируется с позицией чтения stdio, поэтому lseek не требуется).
Дескриптор файла в дочернем процессе не должен зависеть от дескриптора файла в родительском процессе. Это неправильно. В конце концов, FD в разветвлениях — это разные дескрипторы одного и того же FD, точно так же, как то, что вы получили бы с
dup(2)
илиdup2(2)
. Независимым является только закрытие (и дублирование), и когда вы читаете/записываете/lseek(2)
из одного FD, это также влияет на другой FD.