Słowo kluczowe volatile. Obsługa sygnałów w aplikacji Linux/Unix.

Słowo kluczowe volatile istniało już w języku C. Nie jest ono ani niczym nowym w C++, ani też nie rozszerzono jego funkcjonalności. Kiedy piszemy staramy się stworzyć ją bezpieczną. Jeśli chcesz dowiedzieć się jak ważną rolę ono pełni, to zapraszam Cię do dalszej lektury. Przy okazji poznasz kilka rzeczy związanych z programowanie pod Linux. Kwestii bezpieczeństwa powinniśmy poświęcać sporo uwagi. Jednym z czynników poprawnego działania aplikacji jest bezpieczne jej zamykanie. W systemach Linux/Unix bardzo częstą praktyką jest pisanie demonów.

Tak ku woli przypomnienia, są to programy, które działają w tle tzn. kiedy je uruchomimy w konsoli one same natychmiast kończą działanie pozostawiając jednak w pamięci działającą swoją kopię. Efektem takiego zabiegu jest to, że nasza aplikacja działa, a konsola w której ją uruchomiliśmy pozostaje do naszej dyspozycji. Jedynym sposobem zamykania demonów jest wysłanie do nich sygnałów. W praktyce dwoma najważniejszymi sygnałami są SIGINT i SIGTERM. Standardowo jeśli programista nie ustali swojej metody obłsugi tych sygnałów to powodują one natychmiastowe wyjście z aplikacji. Nie zawsze jest to zachowanie satysfakcjonujące, a i bywa niebezpieczne.

Każda aplikacja posiada jakieś zasoby alokując je bądź statycznie podczas uruchamiania, bądź też dynamicznie. Do tych drugich zasobów należą m.in. połączenia sieciowe, bazodanowe, zasoby współdzielone systemowo np.: shared memory blocks, semafory, kolejki komunikatów, etc.

Kiedy coś pożyczasz starasz się jak najszybciej oddać to komuś. Dokładnie taką samą zasadą powinieneś kierować sie pisząc kod. Zwolnij zasoby, które wypożyczyłeś od systemu operacyjnego czy innych aplikacji.

Wróćmy do naszych sygnałów. Napisaliśmy procedurę obsługi sygnałów terminationHandler(…) i zainstalowaliśmy ją w systemie operacyjnym przy użyciu własnej funkcji pomocniczej setAppTerminationHandler(…). Zadaniem naszej funkcji obsługi jest ustawienie pewnej zmiennej na wartość 0, aby zasygnalizować, że aplikacja ma zakończyć działanie. Program główny w warunku pętli while sprawdza wartość tej zmiennej i gdy warunek staje się FALSE sterowanie wychodzi z pętli i w dalszej części następuje tzw. cleanup, czyli zwracanie wypożyczonych zasobów.

...

char bRunning = 0;

...
void terminationHandler(int sigId)

{

	#ifdef DEBUG
	printf("n%s%d%sn", "Received signal #", sigId, ". Server will be stopped.");
	#endif

	bRunning = 0;

}
int main()

{...

	setAppTerminationHandler(terminationHandler);

...

	bRunning = 1;

	while (bRunning)
	{
		if ((conn = accept(sckt, (struct sockaddr*)&caddr, &addr_size)) >= 0)
		{
			switch (onAcceptClient(conn))
			{
			case 0:
				#ifdef DEBUG
				printf("DONEn");
				#endif
				break;
			default:
				#ifdef DEBUG
				printf("Action execution errorn");
				#endif
				break;
			}

			close(conn);
			conn = -1;
		}
	}

...

Kompilujemy naszą aplikację z opcją -D DEBUG. i uruchamiamy go.
Jak narazie wszystko jest Ok, demon się uruchomił, wypisał kilka komunikatów informacyjnych, więc za pomocą rozkazu „ps -a” wypisujemy wszystkie procesy aktualnego użytkownika (czyli nasze). Na liście rzeczywiście jest działająca nasza aplikacja i ma PID np.: 7244. Teraz spróbujmy zakończyć jej działanie wysyłając do niej sygnał: „kill 7244″.

Na ekranie konsoli pojawił się napis

„Received signal #15. Server will be stopped”.

Jednak aplikacja działa dalej. Próbujemy ponownie wysyłając sygnał INT: „kill-SIGINT 7244″.

Na ekranie konsoli pojawił się napis

„Received signal #2. Server will be stopped”.
I znowu nic. Co jest grane ? Przecież procedura obsługi ewidentnie się wykonuje, a aplikacja dalej działa.

Niech zgadne, jesteś ciekaw co tam tak naprawdę się dzieje w kodzie.

Otóż kompilator uznał, że jedynym miejscem, w którym następuje zmiana wartości zmiennej jest program główny w linii przypisania „bRunning = 1;” i wykonał optymalizację warunku pętli, tak że zaczął traktować ten warunek jak warunek stały (tzn czytaj „while (1)”).

Powiesz mi „No dobrze, ale tak jest jeszcze funkcja uchwytu do sygnału i tam jest zmiana wartości tej zmiennej.”

Masz rację. Ale kompilator w kodzie nie znalazł nigdzie jej jawnego wywołania. Nie mógł jej jednak także usunąć z kodu jako zbędnej, bo funkcja ta jest instalowana w systemie operacyjnym (a więc wystąpił dostęp do jej adresu).

Pytasz, jak temu zaradzić, skoro to jest kwestia kompilatora ?

Otóż bardzo prosto. Musimy poprostu zmusić kompilator do tego, aby potraktował tę zmienną jako obiekt pamięciowy o nieprzewidywalnej wartości. Wtedy nie będzie mógł zastosować wobec niej takiej optymalizacji :-D . Zatem zmieniamy deklarację zmiennej na następującą.
volatile char bRunning = 0;

Słowo kluczowe volatile oznacza ulotność. Powinniśmy stosować ją zawsze tam, gdzie wartość zmiennej może zostać zmieniona przez sprzęt, inny program, lub system operacyjny. W przypadku wątków nie będzie to konieczne dlatego, że są one częścią naszej aplikacji i tutaj kompilator jest w stanie stwierdzić czy i gdzie potencjalna zmienna wartości może zajść.

One Response to Słowo kluczowe volatile. Obsługa sygnałów w aplikacji Linux/Unix.

  1. Nareszcie przykład zastosowania volatile. Dzięki!

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *

*

Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>