2. Valgrind/memcheck i zwalnianie zasobów
Review ciąg dalszy. Tym razem zobaczymy jak twoja apka zarządza stertą, file descryptorami, czy coś przez przypadek nie cieknie. Zaczynając od polowania na memory leak-i, uruchamiając apkę:
valgrind --tool=memcheck --leak-check=yes --show-reachable=yes --num-callers=20 --track-fds=yes ./server.out --config conf/config.conf &> valgrind.txt
I ubijając ją po kilknastu sekundach przez Ctrl+C dostajmy 200KB dump z 34 błędami :(
Podsumowanie memcheck-a:
==3740== Memcheck, a memory error detector
==3740== Copyright (C) 2002-2013, and GNU GPL'd, by Julian Seward et al.
==3740== Using Valgrind-3.10.1 and LibVEX; rerun with -h for copyright info
==3740== Command: ./server.out --config conf/config.conf
==3740==
THREADS: 5
Handler 3 started
Handler 7 started
Handler 5 started
==3740==
==3740== FILE DESCRIPTORS: 10 open at exit.
==3740== LEAK SUMMARY:
==3740== definitely lost: 0 bytes in 0 blocks
==3740== indirectly lost: 0 bytes in 0 blocks
==3740== possibly lost: 7,186 bytes in 61 blocks
==3740== still reachable: 9,736 bytes in 146 blocks
==3740== suppressed: 0 bytes in 0 blocks
==3740==
==3740== For counts of detected and suppressed errors, rerun with: -v
==3740== ERROR SUMMARY: 34 errors from 34 contexts (suppressed: 0 from 0)
Wniosek 1: Apka nie radzi sobie z obsługą Ctrl+C - nie ma handlera obsługi sygnału SIGINT, przez co nie są wołane destruktory obiektów,
nie są zamykane deskryptory i w efekcie apka cieknie. IMO warto coś takiego doimplementować bo effort niewielki (kilka linijek z sigaction + volatile) a
profesjonalne serwery takie jak np. nginx i h2o mają taki mechanizm "gratuitous shutdown-u" wbudowany z miejsca.
Uznałem, że trzeba dać apce jeszcze jedną szansę. Zmodyfikowałem delikatnie kod tzn dodałem m_stop = true; w pętli pod Listener::run tak, żeby pętla obróciła się jeden raz i po 10s zakończyła (kiedy strzeli timeout select-a):
while (!m_stop) {
read_fds = m_master;
if (select(m_fdmax + 1, &read_fds, NULL, NULL, &tv) == -1)
throw std::runtime_error(std::string("Main select failed"));
// run through the existing connections looking for data to read
for (int i = 0; i <= m_fdmax; i++) {
if (FD_ISSET(i, &read_fds)) { // we got one!!
if (i == m_listener) {
this->handleNewConnection();
} else {
std::cout << "unknown socket " << i << " writes to main select\n";
}
} // END got new incoming connection
} // for through existing connections
m_stop = true; //<------------------------- dodana linijka
} // main while
Apka nie zawisła jak ostatnio ale sama ładnie zamknęła się po 10s.
Flow wszedł do destruktorów, więc wszystko powinno być OK. No ale nie było. Dump skurczył się do ~70KB. Leaki pozostały :(
Podsumowanie memcheck-a:
==3795==
==3795== FILE DESCRIPTORS: 4 open at exit.
==3795== LEAK SUMMARY:
==3795== definitely lost: 0 bytes in 0 blocks
==3795== indirectly lost: 0 bytes in 0 blocks
==3795== possibly lost: 1,485 bytes in 22 blocks
==3795== still reachable: 5,216 bytes in 75 blocks
==3795== suppressed: 0 bytes in 0 blocks
==3795==
==3795== For counts of detected and suppressed errors, rerun with: -v
==3795== ERROR SUMMARY: 4 errors from 4 contexts (suppressed: 0 from 0)
Wniosek 2: Apka cieknie nawet jeśli jest zamykana po "Bożemu". Czemu? Praktycznie cały dump składa się z call stack-ów lecących z wnętrza protobuf-a.
Przykładowy dump:
== 56 bytes in 1 blocks are still reachable in loss record 31 of 64
==3795== at 0x4C2C100: operator new(unsigned long) (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
==3795== by 0x557E5AB: std::_Rb_tree<std::string, std::pair<std::string const, std::pair<void const*, int> >, std::_Select1st<std::pair<std::string const, std::pair<void const*, int> > >, std::lessstd::string, std::allocator<std::pair<std::string const, std::pair<void const*, int> > > >::M_insert(std::_Rb_tree_node_base*, std::_Rb_tree_node_base*, std::pair<std::string const, std::pair<void const*, int> > const&) (in /usr/lib/x86_64-linux-gnu/libprotobuf.so.9.0.1)
==3795== by 0x557EBAC: google::DescriptorIndex<std::pair<void const*, int> >::AddSymbol(std::string const&, std::pair<void const*, int>) (in /usr/lib/x86_64-linux-gnu/libprotobuf.so.9.0.1)
==3795== by 0x5580C7C: google::DescriptorIndex<std::pair<void const*, int> >::AddFile(google::FileDescriptorProto const&, std::pair<void const*, int>) (in /usr/lib/x86_64-linux-gnu/libprotobuf.so.9.0.1)
==3795== by 0x557C2BD: google::Add(void const*, int) (in /usr/lib/x86_64-linux-gnu/libprotobuf.so.9.0.1)
==3795== by 0x5536DD9: google::InternalAddGeneratedFile(void const*, int) (in /usr/lib/x86_64-linux-gnu/libprotobuf.so.9.0.1)
==3795== by 0x145641: chat::protobuf_AddDesc_chat_2eproto() (in /home/yurai/simpleChatServer/server.out)
==3795== by 0x14B89B: chat::StaticDescriptorInitializer_chat_2eproto() (in /home/yurai/simpleChatServer/server.out)
==3795== by 0x149FA1: __static_initialization_and_destruction_0(int, int) (in /home/yurai/simpleChatServer/server.out)
==3795== by 0x149FDD: _GLOBAL__sub_I__ZN4chat32protobuf_AssignDesc_chat_2eprotoEv (in /home/yurai/simpleChatServer/server.out)
==3795== by 0x15615C: __libc_csu_init (in /home/yurai/simpleChatServer/server.out)
==3795== by 0x5F5D9CE: (below main) (libc-start.c:245)
Okazuje się, że przyczyna to brak wywołania ShutdownProtobufLibrary() na końcu twojego programu. Bez tego wywołania protobuf nie sprząta po sobie, a
memcheck raportuje błędy z wnętrza biblioteki. Źródło:
https://groups.google.com/forum/#!topic/protobuf/HB1c7KN2AM4
Po wprowadzeniu poprawki dump zredukował się do ~1.5KB. Pozostał jedynie problem z socket-em m_listener, który jest tworzony w Listener:
if ((m_listener = socket(p->ai_family, p->ai_socktype, p->ai_protocol)) <0) {
ale nie jest zamykany w Listener::stop. Ostatecznie po naprawieniu tego błędu i dodaniu do Listener::stop linijki:
close(m_listener);
raport z memchecka:
==3908== HEAP SUMMARY:
==3908== in use at exit: 0 bytes in 0 blocks
==3908== total heap usage: 993 allocs, 993 frees, 60,459 bytes allocated
==3908==
==3908== All heap blocks were freed -- no leaks are possible
Memory leaki zniknęły. C.D.N.