LINUX.ORG.RU

Си. Нормально ли не освобождать ресурсы, которые освобождает ОС после завершения программы?

 ,


0

2

Пример:

int main()
{
  int fd;
  char *filename;

  filename = malloc_filename();
  fd = open(filename, O_RDWR);
  if (!fd) {
    perror(filename);
    /* free(filename); */
    return 1;
  }
  
  doing_something(fd);

  /* free(filename); */
  /* close(fd); */
  return 0;
}

Что плохого, если я не сделаю free() и close()? Какие подводные камни? Я раньше думал, что valgrind ругается, если сам не сделал free(), но нет (не исключено, что я неправильно воспользовался valgrind).

LeakSanitizer поругается, глобально станет сложнее искать настоящие утечки (так как выхлоп LSan на выходе ты будешь игнорировать), но ничего критичного. Где-то я даже видел отсутствие освобождения ресурсов на выходе чтобы завершить работу программы как можно быстрее.

a1ba ★★★★
()
Последнее исправление: a1ba (всего исправлений: 1)

Всё зависит от того что это за ресурсы. Если память, то ОС освободит, а если тебе надо что-то сделать, например соединение закрыть, то нет (по крайней мере не со стороны сервера, т.к. там своя ОС и там будет только отвал по таймауту).

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

Если память, то ОС освободит

Файловые дескрипторы, как мне известно, тоже освобождает.

например соединение закрыть, то нет

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

Jullyfish
() автор топика

С завершением процесса ОС освобождает все ресурсы с ним связанные.

Что освобождать, а что нет, зависит от типа ресурса. Скажем, память, можно и не освобождать, т.к. ты ее забрал для работы с программой и когда программа завершилась, то память освободится (но только если это на shared ресурс, который ты делишь с другими процессами).

Освобождение некоторых ресурсов может иметь дополнительные нюансы, может так оказаться, что ты не можешь закрыть какой-то ресурс, т.к. он еще занят или оборудование подыхает или еще что-то. В зависимости от кода возврата попытки освобождения ресурса может потребоваться сделать еще какие-то действия.

Поэтому, на мой взгляд, лучше все ресурсы освобождать самостоятельно, и когда программа закончилась, то ты будешь уверен, что все ресурсы точно УЖЕ освобождены.

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

soomrack ★★★★★
()

Есть разные способы на это смотреть. Один говорит: The building is being demolished. Don’t bother sweeping the floor and emptying the trash cans and erasing the whiteboards. And don’t line up at the exit to the building so everybody can move their in/out magnet to out. All you’re doing is making the demolition team wait for you to finish these pointless housecleaning tasks. Как выше уже правильно сказали, тогда на Вас будут ругаться Valgrind и LeakSanitizer. (Тогда обычно считают, что блоки памяти, указатели на которые «still reachable» - это не утечка, а оптимизация. В частности, LeakSanitizer не будет ругаться, если на момент завершения процесса указатель на блок памяти ещё существовал.) Так делают на практике, в т.ч. уважаемые программные проекты:

$ R -d valgrind -e 'q()'
...
==12782== LEAK SUMMARY:
==12782==    definitely lost: 0 bytes in 0 blocks
==12782==    indirectly lost: 0 bytes in 0 blocks
==12782==      possibly lost: 0 bytes in 0 blocks
==12782==    still reachable: 40,854,854 bytes in 9,251 blocks

Иногда это приводит к непредвиденным последствиям. Например, если собирать целый проект с -fsanitize=address,undefined, а там внутренние программы или части configure за собой не подбирают, LeakSanitizer это заметит, заставит процесс завершиться с ненулевым кодом выхода, и вся сборка накроется.

В библиотечном коде лучше потратить чуть больше времени и явно за собой всё подобрать.

Однажды я конвертировал гигантскую электронную книгу при помощи ebook-convert. Процесс занял всю оперативную память и ещё немного swap на SSD. Когда файл уже был готов, а процесс ещё полчаса не завершался, я подключился к нему при помощи strace, чтобы посмотреть, что он там ещё делает. Процесс делал много-много системных вызовов munmap(), освобождая выделенную память.

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

Нет, ты не понимаешь как работает протокол tcp. Когда установлено соединение с сервером каким-то, то на сервере (у тебя вообще нет к нему доступа, он физически стоит у другого челика, ну или есть, но твоя ОС там памятью не рулит) запоминается момент когда ты подключился/когда ты последний раз туда что-то отправлял. На это соединение выделяется память (мало, но выделяется). Если твоя программа завершает работу с сервером штатно, то она кидает пакет с меткой FIN, сервер должен ответить ACK и кинуть пакет FIN, после чего ты тоже отвечаешь ACK и освобождаешь ресурсы. Если у тебя какие-то проблемы и ты не можешь закрываться штатно, то ты должен кинуть пакет RST и после этого осовободить ресурсы, но RST может не дойти до сервера, например из-за помех, пропажи питания где-то на линии и следовательно гарантии доставки у него тоже нет. При краше нормальные ОС также должны штатно закрыть за тобой подключение по описанному алгоритму. Но они не сделают это в том случае, если сокет ушёл другому всё ещё живому процессу (например через fork). Понятно что микроконтроллеры тебе таких гарантий не дадут и там ты всё должен ручками делать. Более жёсткие проблемы будут у протоколов поверх udp и части прикладных протоколов поверх tcp (т.к. никаких обязанностей у ОС по корректному завершению их работы нет, да и она ничего про них не знает). Проблемы могут быть у разделяемой памяти и в именных пайпах, файллоки за тебя тоже никто не почистит.

peregrine ★★★★★
()

Работать будет, но такой код сложнее скалировать с ростом программы. Не только динамически аллоцируемых переменных станет больше, но и часть из них разъедется по отдельным функциям. Ты за всем этим тогда должен следить и решать, что нужно освобождать, а что «в животе перемешается». Кому оно надо? Мог бы сразу правильно написать и не выкабениваться.

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

Нет, ты не понимаешь как работает протокол tcp.

Правда, которая ранит прямо в сердце.

Спасибо за информацию! Выглядит очень сложно, потом ещё раз перечитаю. :^)

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

Работать будет, но такой код сложнее скалировать с ростом программы.

Мог бы сразу правильно написать и не выкабениваться.

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

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

В TLS например тоже надо посылать специальное сообщение при закрытии соединения (close_notify или как там его). Иначе злоумышленник посередине может просто разорвать связь, одна из сторон подумает что данные закончились, и уйдёт.

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

Нет, это ты не понимаешь как работает TCP-стек (и вообще доступ к системным ресурсам) в современных ОС.

Если твоя программа завершает работу с сервером штатно, то она кидает пакет с меткой FIN, сервер должен ответить ACK и кинуть пакет FIN, после чего ты тоже отвечаешь ACK и освобождаешь ресурсы. Если у тебя какие-то проблемы и ты не можешь закрываться штатно, то ты должен кинуть пакет RST и после этого осовободить ресурсы

Нет, программа никаких ни FIN ни RST не кидает и вообще не вникает во все эти детали.

При краше нормальные ОС также должны штатно закрыть за тобой подключение по описанному алгоритму. Но они не сделают это в том случае, если сокет ушёл другому всё ещё живому процессу (например через fork).

Крайне плохо сформулированное и вводящее в заблуждение описание ситуации.

Правильно так: программа может вызывать close() на сокет, сообщая этим ОС, что данный сокет ей больше не нужен. Точно то же самое произойдёт, если программа просто завершится по любой причине. Внимание: close не шлёт ни FIN, ни RST! Ещё раз, это сисколл «мне не нужен этот сокет». Сокет, как ты заметил, может принадлежать больше чем одному процессу (а ещё он может быть представлен двумя, тремя итд дескрипторами в одном процессе или находиться «на лету» в процессе пересылки между процессами, там у него дескриптора нет), и так вот, ОС имеет счётчик этих ссылок на сокет. Когда ни одной ссылки не осталось - соединение начинает закрываться (шлются все это FIN/RST). Закрывание соединения делается в ОС вне контекста какой либо программы, оно делается как раз потому, что ни одна программа больше этот сокет иметь не желает.

FIN можно послать заранее с помощью shutdown() явной командой из программы, но это не обязательно. Но после этого FIN-а сокет всё ещё будет открытым. Часть его данных (связанная с конкретно tcp-протоколом) освободится когда пришлют ответный FIN, но до конца сокет будет освобождён только когда его все закроют через close() или завершатся, а так же не останется его экземпляров в процессе пересылки между процессами.

Более жёсткие проблемы будут у протоколов поверх udp

Это уже другая тема. С точки зрения ОС сокет будет закрыт и никакие ресурсы тратить не будет. Кто-то на той стороне, возможно, об этом не узнает, но чтоб он узнал надо ему что-то отправить через send(), а не просто «освобождать соединение».

firkax ★★★★★
()

Перед завершением именно программы делать free() и close() не обязательно.

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

Примером могло бы быть fwrite()+fclose(), ведь fwrite пишет в файл не сразу, а сначала хранит в своём буфере, однако тут как раз нет: libc внутри себя хранит список всего что ты открыл через fopen(), и при выходе из программы (в Си есть хуки на выход из программы, ставятся через atexit()) их перебирает и сбрасывает все неотправленные данные как если бы ты вызвал fclose(). Но я такие фокусы (неявное выполнение чего-то на выходе) в целом не одобряю, лучше требовать явного вызова завершающих функций.

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

Согласен, с tcp ошибся (я на низком уровне его только на мк гонял, а там такого нет). Остальное вроде как нет.

peregrine ★★★★★
()
Последнее исправление: peregrine (всего исправлений: 2)
Ответ на: комментарий от Jullyfish

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

«лучше тридцать раз перебздеть, чем один раз обосраться !!»
но минус в скорости создания и вычищении мелких косяков и варнингов потому современное скоростное погромирование ложит на это блинный болт с правой резьбой.

pfg ★★★★★
()

При обучении студентов я использую такие размышления.

Вот есть игра doom3, там попадаются игровые автоматы, типа такого. Не помню что на них были за игры, но ничего не мешает воткнуть в них первый дум например, т.е. не заглушку, а полноценное когда-то написанное самостоятельное приложение, желательно с минимальными изменениями. Получается внутри doom3 по копии doom1 на каждый игровой автомат на уровне, и их нужно создавать/убивать со сменой уровней, а также сериализовать/десериализовать при save/load - вот тут все косяки дизайна как-то глобальное состояние и текущие ресурсы, становятся очевидны. Нормально написанной игре ты просто подменишь инпут, вывод изображения и звука, и способ доступа к стейту. А хреново написанная будет крашиться из-за рейсов, показывать одно и то же на всех автоматах и сбрасываться после save/load.

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

Что плохого, если я не сделаю free() и close()? Какие подводные камни?

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

Тебе лень писать close/free в каждой ветке обработки ошибок, и это очень правильно, потому что любой зрелый программист заботится о своей эффективности и о чистоте кода, а особенно его корректности, т.к. понимает что любой бойлерплейт скрывает ошибки. Поэтому пора взять полноценный ЯП c RAII. На котором код из твоего примера будет занимать ровно одну строчку - doing_something(open(malloc_filename())) с точностью до пары символов, описывать ровно то что делает, при этом всегда корректно обрабатывать ошибки и освобождать ресурсы.

anonymous
()

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

Возможность забить болт на контроль за ресурсами вовсе не означает что это стоит делать.

ЗЫ: Это не только к сишечке относится, кстати, и более того, к ней это относится несколько меньше чем к многим другим языкам, где пишущий привык (или индоктринирован), что кто-то за него все ресурсы непременно освободит и что ему парится о такой фигне вообще не нужно.

Stanson ★★★★★
()
Последнее исправление: Stanson (всего исправлений: 2)

На языке, на котором я пишу внутренние ресурсы практически все освобождаются автоматически, во первых создаются типа таблицы открытых ресурсов и закрытие сопровождается пробегом по этой таблице. Если структуры/списки структур открыты в функции, то добавляется скрытый флаг, вызывающий процесс освобождения памяти всех локальных ресурсов. Другое дело использование API, тут система может создать ресурс зарегистрированный в самой системе и язык не контролирует такие объекты. Если прога создаёт один объект в памяти, размером пол-мегабайта я раньше мог не обращать на это внимание, но в какой-то момент я начал очищать ресурсы, с момента когда стал писать проги с циклом создающие например сотню ресурсов, а другой на этой проге может создать миллион, мы же не знаем как другие применяет по минимуму или по максимуму, если прога позволяет сделать миллион объектов, то надо просто принять эту ситуацию. И я просто сделал список всех открываемых в цикле ресурсов, ну как это сделал автор языка для своих ресурсов. И когда прога завершается, то на выходе просто пробегаю по списку дескрипторов и по отдельным индивидуальным переменным с дескрипторами. Короче освобождаю всё.

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

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

AZJIO
()
Последнее исправление: AZJIO (всего исправлений: 3)

Что плохого, если я не сделаю free() и close()?

сразу ничего..плохое начнётся позже, когда hello-word разрастётся например до класса, а привычка не освобождать ресурсы останется

MKuznetsov ★★★★★
()
Ответ на: комментарий от ya-betmen

Только под виндой так не делай. Неудаляемые без ребута файлы - боль.

Это как раз от тех процессов которые по каким-то причинам не смогли завершиться (зависли на системном вызове), или вообще открыты в ядре винды. Автозакрытие дексрипторов при выходе процесса там такое же.

GPFault ★★★
()
  • Markdown
Пустая строка (два раза Enter) начинает новый абзац. Знак '>' в начале абзаца выделяет абзац курсивом цитирования.
Внимание: прочитайте описание разметки Markdown.
Используйте Ctrl-Enter для размещения комментария