LINUX.ORG.RU

Помогите понять странную работу программы

 


1

2

Здравствуйте.

Есть программа на СИ (линукс).

Вот часть функционала в функции маин...

...
  for(;;)
  {
    rat_rub = fopen("/tmp/file.txt", "r"); 
    if(rat_rub == NULL) error_log("rat_rub!");
    while(fgets(buff, 255, rat_rub) != NULL)
    {
      time_creat = strtoul(buff, NULL, 0);
      printf("buff: %lu %s\n", time_creat, buff);

      if(time_creat > time_res)
      {
        printf("SEND: %s\n", buff);
        SendMessage(namechatid, buff, 1);
        //sleep(2); 
      }
    }

    if(fclose(rat_rub) == -1) error_log("close rat_rub_func.");
    time_res = time_creat;
  }
...

Функция SendMessage(namechatid, buff, 1); вызавет функцию, в которой происходит форк, выполняется действие и выход из этой функции - exit(0);

По условиям, функция должна вызваться три раза (из файла читается построчно три строки и каждая строка поочерёдно отправляется в SendMessage).

Вопрос вот в чём: если sleep(2) закомментировано (как в примере), то всё работает нормально. А вот если sleep(2) раскомментировать, то функция SendMessage начинает вызывать большее количество раз, в частности 10 раз.

Как такое может происходить? Как sleep(2) на это влияет?


Без определения SendMessage() как-то не скажешь ничего.

Makhno
()

Функция SendMessage(namechatid, buff, 1); вызавет функцию, в которой происходит форк, выполняется действие и выход из этой функции - exit(0);

По условиям, функция должна вызваться три раза

Выход по exit откуда? Из форка или из функции SendMessage? Если из функции, то больше одного раза она вызываться не может, т. к. при каждом входе в эту функцию программа будет завершать работу.

Вопрос вот в чём: если sleep(2) закомментировано (как в примере), то всё работает нормально. А вот если sleep(2) раскомментировать, то функция SendMessage начинает вызывать большее количество раз, в частности 10 раз.

Как такое может происходить? Как sleep(2) на это влияет?

Много чего на это может влиять. Какая программа и как часто создаёт и обновляет файл /tmp/file.txt? Модифицирует ли его форк, создаваемый в SendMessage, и, если да, то каким образом? Изменяется ли где-то в коде (в той же SendMessage) текущая позиция указателя в открытом файле вызовом fseek или чего-то в этом роде? За эти 2 секунды третий процесс или твой форк может что-то добавить в файл, в результате чего число валидных записей увеличится с 3 до 10. Надо смотреть, что там происходит.

aureliano15 ★★
()
Ответ на: комментарий от aureliano15
void SendMessage(char *chat_id, char *send_text, int cod) 
{
    pid_t smpid;   
    smpid = fork();
    if(smpid == 0) 
     { 
       char json_str[BUFSIZE] = {0,}; 

       ...здесь send_text отправляется в сеть

       if(close(sd) == -1) error_log("close3 sd in SM.");
       exit(0);

     } // END FORK
     
} // END SendMessage

Файл не обновляется. Просто файл с тремя строчками.

Форк ничего не модифицирует, он просто отправляет полученную строку в сеть.

stD
() автор топика
Ответ на: комментарий от stD

Вообще всё убрал из SendMessage, оставил только это...

void SendMessage(char *chat_id, char *send_text, int cod) 
{
    pid_t smpid;   
    smpid = fork();
    if(smpid == 0) 
     { 

       printf("FFFFFFFFFF: %s\n", send_text);
       exit(0);

     } // END FORK

} // END SendMessage

В итоге, получается следующее: сначала отправлется первая строка из файла, затем вторая, затем третья - то есть всё ок. Затем отправляется вторая строка, затем третья, итак три раза.

То есть, три раза отправляются вторая и третья строки из файла.

stD
() автор топика
static void SendMessage(char *chat_id, char *send_text, int cod, FILE *fd) 
{
    pid_t smpid;   
    smpid = fork();
    if(smpid == 0) 
     { 
       fclose(fd);
       printf("FFFFFFFFFF: %s\n", send_text);
       exit(0);

     } // END FORK

} // END SendMessage
...
SendMessage(namechatid, buff, 1, rat_rub);
gman
()
Ответ на: комментарий от aureliano15

Я уже убрал изменения в файле и убрал time_res.

Если убирать из функции SendMessage форк, то всё работает нормально. С форком по два раза повторяется вывод второй и третьей строки.

stD
() автор топика
Ответ на: комментарий от gman

gman Спасибо.

С закрытием файла в форке всё работает как надо.

Я правильно понимаю, что форк создавал свою копию цикла while (из функции main) и уже в форке происходило дублирующее чтение файла?

stD
() автор топика
Ответ на: комментарий от stD

Нет, «копию цикла» fork() не создаёт. fork() создаёт копии дескрипторов, памяти, etc. Тем не менее незакрытый в дочернем процессе указатель на stream rat_rub приводит к некорректному поведению fgets() в родительском процессе. Затрудняюсь сказать в чём конкретно здесь причина.

gman
()
Последнее исправление: gman (всего исправлений: 1)
Ответ на: комментарий от gman

Большое спасибо. В целом суть мне понятна.

stD
() автор топика
Ответ на: комментарий от xaizek

Убрал закрытие дескриптора предложенное gman из функции SendMessage и использовал _exit(0); вместо exit(0);, работает нормально, без повторений.

Объясните пожалуйста, почему так?

stD
() автор топика
Ответ на: комментарий от stD

Вылезло ещё кое-что...

Если прогонять программу через valgrind, то:

В случае с exit(0); и вариантом gman, всё работает хорошо. Утечек памяти нет, и дублирующих чтений нет.

В случае с _exit(0); есть утечка, и дублирующее чтение.

stD
() автор топика
Ответ на: комментарий от stD

В отличии от _Exit, exit делает дескрипторам fflush. Ничего лишний раз не отправлялось, IO буферированно и буферы в оригинальном процессе накапливались и в нём же и сбрасывались, но также они сбрасывались и в дочерних процессах при их завершении. Отсюда и дубли.

xaizek ★★★★★
()
Ответ на: комментарий от stD

В случае с _exit(0); есть утечка

Дескриптора?

и дублирующее чтение.

Что это такое?

Вообще fclose(fd); на буфер stdout не должен никак влиять, так что я тоже не до конца понимаю, в чём дело.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Дескриптора?

Вот утечка:

 HEAP SUMMARY:
==18318==     in use at exit: 552 bytes in 1 blocks
==18318==   total heap usage: 2,063 allocs, 2,062 frees, 246,008 bytes allocated
==18318== 
==18318== 552 bytes in 1 blocks are still reachable in loss record 1 of 1
==18318==    at 0x4C2DB8F: malloc (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==18318==    by 0x5771CDC: __fopen_internal (iofopen.c:69)
==18318==    by 0x402698: main (stmbot.c:397)
==18318== 
==18318== LEAK SUMMARY:
==18318==    definitely lost: 0 bytes in 0 blocks
==18318==    indirectly lost: 0 bytes in 0 blocks
==18318==      possibly lost: 0 bytes in 0 blocks
==18318==    still reachable: 552 bytes in 1 blocks
==18318==         suppressed: 0 bytes in 0 blocks

Точнее, возможная утечка.

Строка stmbot.c:397 это вот это:

rat_rub = fopen("/tmp/file.txt", "r");

и дублирующее чтение.

Имелось в виду повторная отправка строки (то, чего не должно происходить).

stD
() автор топика
Последнее исправление: stD (всего исправлений: 1)
Ответ на: комментарий от stD

Достижимое, это не утечка. Но поведение вывода странное. Можно что ли печатать pid процесса, может это прояснит что-то. И/или явно fflush вызывать до форка попробовать.

xaizek ★★★★★
()
Ответ на: комментарий от xaizek

Добавил fflush перед вызовом SendMessage...

fflush (rat_rub);
SendMessage(namechatid, buff, 1);
sleep(2); 

...все проблемы исчезли.

Всё работает как положено, и в случае с _Exit, и в случае с exit.

Посоветуйте, что оставить в форке, _Exit или exit?

stD
() автор топика
Ответ на: комментарий от aureliano15

в бесконечном цикле for

Это правильный аналог while(1) же. Такое часто применяется для тех же управляющих циклов в которых, например, происходит чтение данных и выполнение соответствующих действий. А с выходом из этого цикла завершается и программа.

saahriktu ★★★★★
()
Ответ на: комментарий от saahriktu

Да это понятно. Я спросил, зачем несколько раз открывать file.txt, если он не меняется, или всё же меняется? Тогда это могло бы объяснить наблюдаемый побочный эффект. Но сейчас ясно, что побочный эффект как-то связан с тем, что файл открыт в обоих процессах. Хотя как это может быть связано с этим — совершенно непонятно, ведь процесс — абсолютно изолированная сущность. Да, он наследует дескрипторы, но после наследования это уже другие дескрипторы, пусть даже и на те же файлы, как и буферы уже другие. Самому аж интересно стало. Надо будет потестировать. Попробую кастануть i-rinat, если его не забанили после недавней драмы. Может у него какие мысли есть на этот счёт?

aureliano15 ★★
()
Ответ на: комментарий от aureliano15

Благодарю всех за помощь.

stD
() автор топика
Ответ на: комментарий от stD

что оставить в форке, _Exit или exit?

Я бы выбрал первое, так как эта функция для этого и существует.

if(time_creat > time_res)

sleep(2)

Что-то мне подсказывает, что всё таки оригинальный процесс шалит (pid в выводе показали бы это). Может попробовать дожидаться завершения форка? Или как-то отмечать уже отправленные сообщения.

xaizek ★★★★★
()
Последнее исправление: xaizek (всего исправлений: 1)
Ответ на: комментарий от aureliano15

Может у него какие мысли есть на этот счёт?

После fork'а потомок наследует все открытые файловые дескрипторы. Поэтому если на момент форка были открытые файловые потоки FILE, потомок получит копию структур в памяти и копию скрытого в FILE файлового дескриптора. Самое интересное тут заключается в том, что именно копию дескриптора, а не копию всех ядерных структур, связанных с этим дескриптором. Дескриптор в потомке указывает на ту же структуру, что и дескриптор в родителе. Поэтому если потомок сделает lseek, родитель это тоже увидит. Вот пример программки, чтобы поиграться:

#include <assert.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <unistd.h>

int main(void) {
  int fd = open("tmpfile", O_RDWR | O_CREAT, 0666);
  assert(fd >= 0);

  const char *buf = "hello, world\n";
  int ret = write(fd, buf, strlen(buf));

  int pos = lseek(fd, 0, SEEK_CUR);
  printf("Pre-fork fd position: %d\n", pos);

  ret = fork();
  assert(ret != -1);
  if (ret == 0) {
    printf("Child waits for a second.\n");
    sleep(1);
    printf("Child moves fd position to 0.\n");
    lseek(fd, 0, SEEK_SET);
    printf("Child moved fd position to 0.\n");
    printf("Child terminates.\n");
    exit(0);
  }

  pos = lseek(fd, 0, SEEK_CUR);
  printf("After-fork fd position in parent: %d\n", pos);
  printf("Parent waits for two seconds.\n");
  sleep(2);
  pos = lseek(fd, 0, SEEK_CUR);
  printf("After-fork fd position in parent: %d\n", pos);

  close(fd);
  return 0;
}

Если её запустить, получится:

Pre-fork fd position: 13
After-fork fd position in parent: 13
Parent waits for two seconds.
Child waits for a second.
Child moves fd position to 0.
Child moved fd position to 0.
Child terminates.
After-fork fd position in parent: 0

И финальная часть пазла. При каких-то там условиях закрытие файлового потока FILE вызывает lseek, который двигает позицию в файле. Кажется, она двигается в «консистентное» состояние. То есть, если файловый поток ради скорости прочитал килобайт, а пользователь через fgets получил одну строку, которая меньше килобайта, реальная позиция в файле будет 1024, тогда как пользователь ожидал бы, что позиция будет там, где прочитанная строка оканчивается. Вот этот lseek прыгает на ожидаемую позицию. Но я в детали не вдавался, так что тут мог наврать. Факт в том, что lseek иногда вызывается.

Другими словами, наследовать FILE через форк не рекомендуется.

i-rinat ★★★★★
()
Ответ на: комментарий от i-rinat

Факт в том, что lseek иногда вызывается.

Вызывается не по воле программиста?

Другими словами, наследовать FILE через форк не рекомендуется.

То есть нужно открыть файл, прочесть строку, закрыть файл, и только потом передать строку в форк?

stD
() автор топика
Ответ на: комментарий от stD

Если в тестовой программке с fopen+fwrite не делать fclose, а сразу выходить, видно, что lseek вызывается. С fclose такого вызова уже нет. Возможно, дело в подчистке буферов, которая происходит во время вызова exit. Возможно, какие-то другие условия ещё влияют.

Дальше копать стало лениво, поэтому не копал. Если интересно, попробуй посмотреть в glibc. Но самым правильным будет всё же смотреть в текст стандарта на предмет взаимодействия FILE и fork.

Если хочешь гарантий по-быстрому, не используй файловые потоки Си, используй напрямую open/read/write/close.

i-rinat ★★★★★
()
Последнее исправление: i-rinat (всего исправлений: 1)
Ответ на: комментарий от i-rinat

Дескриптор в потомке указывает на ту же структуру, что и дескриптор в родителе.

То есть будет два дескриптора (один у родителя, другой у потомка), но указывать они будут на одну и ту же структуру?

stD
() автор топика
Ответ на: комментарий от stD

Можно вопрос на окончательное понимание?

То есть происходит (или может произойти) следующее:

В родителе открывается файл, читается строка, строка передаётся в функцию с форком, у форка появляется копия дескриптора...

Тогда можно предположить, что форк делает lseek на начало той строки, которую я уже передавал (это так же отразится и в родителе), и соответственно родитель при следующей передаче строки, передаст опять ту же строку?

Извиняюсь за корявую формулировку.

Кусок кода родителя:

// родитель передаёт строку
while(fgets(buff, 255, rat_rub) != NULL)
    {
        SendMessage(namechatid, buff, 1);
        sleep(2); 

    }
stD
() автор топика
Ответ на: комментарий от i-rinat

именно копию дескриптора, а не копию всех ядерных структур, связанных с этим дескриптором. Дескриптор в потомке указывает на ту же структуру, что и дескриптор в родителе. Поэтому если потомок сделает lseek, родитель это тоже увидит.

Спасибо за ликбез. Значит, не так уж эти процессы изолированы друг от друга, как я думал. Век живи, — век учись.

Но самым правильным будет всё же смотреть в текст стандарта на предмет взаимодействия FILE и fork.

Думаю, это бесперспективно. FILE относится к стандарту Си, где ничего про fork нет и быть не может (но я всё-таки поискал это слово в стандарте c11 и, ожидаемо, ничего не нашёл). А fork — это unix/posix. Там я нашёл такое:

The child process shall have its own copy of the parent's file descriptors. Each of the child's file descriptors shall refer to the same open file description with the corresponding file descriptor of the parent.

(Источник: http://pubs.opengroup.org/onlinepubs/009695399/functions/fork.html)

Т. е. подтверждается сказанное тобой, но если бы я это прочитал не в связи с подобным поведением, никогда бы не догадался, решив, что «same open file description» — такое же описание, но в другом месте. :-)

aureliano15 ★★
()
Ответ на: комментарий от stD

В родителе открывается файл, читается строка, строка передаётся в функцию с форком, у форка появляется копия дескриптора...

Тогда можно предположить, что форк делает lseek на начало той строки, которую я уже передавал (это так же отразится и в родителе), и соответственно родитель при следующей передаче строки, передаст опять ту же строку?

Примерно так.

aureliano15 ★★
()
Ответ на: комментарий от stD

Когда ты делаешь fopen, позиция в файле — 0. Когда ты делаешь fgets в первый раз, libc читает не ровно строчку, она читает сразу много байт. Скажем, 1024. Или сколько есть. Потом, когда ты вызываешь fgets второй раз, реального чтения из файла может не произойти, потому что необходимые данные уже будут в буфере. В этом и суть использования файловых потоков libc — они экономят системные вызовы. В какой-то момент очередной fgets доходит до конца буфера, но видит, что строка не закончилась. И тогда libc делает ещё один read. Если перед этим кто-то сделал lseek на другую позицию, с точки зрения пользователя fgets в файл как бы подклеили в конец ещё одну копию части файла. С её точки зрения файл продолжается, но на деле она заново читает уже то, что было ранее прочитано. Конкретное соотношение мест подклеивания и размеров подклеиваемой части зависит от размера внутреннего буфера, связанного с конкретным экземпляром FILE, размера файла, и вообще способом реализации буферизации.

i-rinat ★★★★★
()
Ответ на: комментарий от i-rinat

как бы подклеили в конец ещё одну копию части файла.

В точку. Повторяются как раз вторая и третья строки.

Спасибо большое за разъяснения. Сохраню Ваш пост у себя в заметках.

stD
() автор топика
Ответ на: комментарий от stD

У меня сейчас сделано вот так...

while(fgets(buff, 255, rat_rub) != NULL)
    {
        fflush(rat_rub);
        SendMessage(namechatid, buff, 1);
        sleep(2); 

    }

...очищается буфер потока.

Как в таком случае работает fgets?

stD
() автор топика
Ответ на: комментарий от stD

Будет лучше, если ты посмотришь в исходники glibc сам.

i-rinat ★★★★★
()
Ответ на: комментарий от stD

Парень, ты либо сам экспериментируй и не только с glibc, но и с musl ибо не фак что glibc стандарт чтёр как евангеле. Или пиши/переписывай код в предсказуемом линейном виде, закрывай ресурсы перед форком, переоткрывай только тогда когда явно используешь, не раздавай один ресурс множествам потомков на запись/изменение/позицию чтения скорости от этого не будет. Просто дело в том что всяких нюасов везде тонны и разбирать каждый их них будет вредно, нужно самому на грабли наступать. Или читать стандарт как отче наш и полагатся только на него и не выходить за его рамки для вещей которые должны работать железобетонно.

anonymous
()
Ответ на: комментарий от stD

...очищается буфер потока.

Как в таком случае работает fgets?

Ещё можно потестировать свою программу с fflush на разных файлах в разных ситуациях. Но даже если всё будет работать, нет гарантий, что это не изменится при переходе к другому ядру/версии ядра или к другой версии стандартной библиотеки. Тут надо найти в стандарте однозначное указание на то, что при выполнении fflush подобные эффекты невозможны. Но т. к. стандартная библиотека относится к языку, а fork - это стандарт posix, к языку непосредственного отношения не имеющий, то вряд ли в каком-то из стандартов оговариваются эти тонкости на стыке 2 разных стандартов. Поэтому я бы просто использовал низкоуровневые функции open, read и close вместо их аналогов из stdio. Ну или в дочерних процессах вместо exit вызывал бы _exit(), как предлагает xaizek. Но я не уверен, что она 100% огородит от всех возможных побочных эффектов во всех возможных случаях.

aureliano15 ★★
()
Ответ на: комментарий от anonymous

переоткрывай только тогда когда явно используешь

Ну а это ещё зачем? Вот у автора есть сеть для SendMessage. Ему этот, скажем, сокет переоткрывать?

Но это ещё что, он не нарвался на смешивание потоков от параллельных send-ов, так как строки короткие и в один write влезают :)

vodz ★★★★★
()

Развели в треде кашу, закрывай ресурсы, юзай в паренте сисколы... _Эксит для того и сделали, чтобы всем этим не заниматься. После форка не трогаешь никакие старые FILE в чилде, а для выхода вызываешь флуш и _Exit(), чтобы exit() ничего не трогал. Кроме прочего не вызовутся atexit-ы, которые могут понаставить всякие бдб, скулайты и пр. абсолютно внезапные интерфейсы, обычными файлами дело не заканчивается.

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

exit() надежнее

Ну это вообще пушка.

anonymous
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.