int21h

Procesy a signály

Toto je první díl seriálu, ve kterém bych chtěl přiblížit na základní formě složitější techniky programování na unixu. Je to vlastně odpověď na články Martina Luxe o výhodách a novinkách ve FreePascalu. Asi už očekáváte, že moje články budou pro unixové systémy a v jazyku c.

Doufám, že mi nemáte za zlé, že se na int21h - převážně dosáckém to časopise vyskytují stále více články o linuxu. Mě linux naprosto chytil a myslím, že linux má s dosem dost společného (i když dos s linuxem ne:) Když jsem já poprvé pracoval s linuxem, tak mi hned připadalo, že má vše, co mi na dosu chybělo. Brzy mě tato iluze přešla, když jsem si uvědomil, že je to není jen jiný os, ale naprosto jiný způsob přemýšlení o počítači a informačních technologiích. To je samozřejmě win32 nebo apple taky, ale to se dá srovnávat jen z pohledu uživatele. Jak dojde na programování, tak linux a unix všeobecně stojí vysoko nad ostatníma. V tomhle seriálu bych se chtěl zaměřit právě na pokročilejší techniky, které dělají z linuxu linux. Za vše co vím z této oblasti vděčím knize Pokročilé Programování V Operačním Systému Linux od společnosti CodeSourcery u nás vydané v SoftPress.

Procesy

Tohle už možná znáte z windows. Program je jen soubor. Po spuštění programu se do systému zavede proces, který vykonává veškerou činnost. Na linuxu se s procesy setkáváme častokrát i jako uživatel. Ve windows se na ně můžete podívat jedině snad přes Ctrl+Alt+Delete. Každému procesu jádro přidělí takzvaný pid (Process ID), což je číslo, přes které můžeme s procesem dále komunikovat a určovat ho. Číslo pid je neurčitelné. Dále má proces taky číslo ppid (Parent Process ID), neboli pid rodičovského procesu. To je číslo procesu, který ho spustil. Z toho plyne, že procesy tvoří velký strom, protože každý process má svého rodiče (parent) a potomky (child). Hlavní proces se jmenuje init a má vždy číslo 1 (pozn. v knize Rozumíme Unixu od Jona Lassera se píše, že "systém někdy zapomene počítat od nuly, jak je v počítačové branži zvykem a začne počítat od jedničky", což je podle mě blbost, protože kdyby měl pid 0, tak jaký by měl pak ppid?). init je tedy první proces, který se v systému spustí a všechny procesy jsou jeho potomci. Když například zadáme příkaz $ ps -e -o pid,ppid,command, tak můžeme stopovat předky processu ps. Např. ps<-bash

Tak to byla teorie a teď programování. pid a ppid programu můžeme zjistit přes funkci getpid() a getppid(), které jsou obsaženy v knihovně unistd.h. Funkce vrací typ pid_t, jehož definice je v sys/types.h. Píšu to proto, že tyto dvě knihovny budou nejčastější.

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>


int main() {
        pid_t pid=getpid();
	pid_t ppid=getppid;
	printf("PID tohoto procesu: %d",(int)pid);
        printf("PID parent procesu: %d",(int)ppid);
        return 0;
}  
Tyto funkce nemají moc využití. Hlavně se používají, jak říká manuálka, k vytváření unikátních jmen temp souborů.

Jeden program může samozřejmě spustit i více procesů. Tedy jeden parent a mnoho child procesů. Na to slouží příkaz fork(), který rozdělí program na dva identické procesy.
Programování procesů už vyžaduje naprosto jiné přemýšlení o programování. Už nespravujeme jeden běžící úsek programů, ale dva a to paralelně. Po rozdělení procesu už ani nevíme, který skončí dřív, nebo jako bude mít pid.
pid_t child;
printf("Main program; pid %d; ppid %d",(int)getpid(),(int)getppid());
child=fork();
if (child!=0) {
	printf("Parent process; pid %d; ppid %d",(int)getpid(),(int)getppid());
} else {
	printf("Child process; pid %d; ppid %d",(int)getpid(),(int)getppid());
}  
vypíše něco jako:
Main program; pid: 1683; ppid: 1482
Parent process; pid: 1683
Child process; pid: 1684; ppid: 1683  
Většinou se procesy používají k tomu, aby váš program spustil jiný program. Jde to samozřejmě udělat přes známy system(). Ten je ovšem na jiné bázi: předá zadaný parametr příkazovému interpretru a ten ho spustí. To je krajně nebezpečné a většina rootshellů spoléhá právě na používání system(). Aby jsme docílili bezpečného spuštění, tak musíme provést tento postup: rozdělit process na parent a child a v child nahradit vykonávání kódu jiným procesem přes funkci exec()
pid_t child;
char *args[]={
	"ls",
	"-l",
	NULL
};
printf("Main program: %d",(int)getpid());
child=fork();
if (child!=0) {
	return 0;
} else {
	printf("Child program ls -l: %d",(int)getpid());
	execvp("ls",args);
}
return 0;  
Poznámka k execvp
v - exec přijímá seznam argumentů
p - exec hledá program v PATH
teď dvě situace:
Co když vytvoří program proces child a hned se ukončí?
V této situaci se child proces předá procesu init. (ppid=1)
A další situace
Child proces sice skončí, ale parent pokračuje v práci.
Parent proces má povinnost uklízet po sobě child procesy. Takže pokud child skončí, ale není uklizen, tak se z něj stane tzv. proces zombie. Tenhle zombie zůstává v systému až do té doby, než se ukončí parent. Samozřejmě se jedná o chybu, která má řešení ve funkci wait.
Funkce wait počká, až se child ukončí a pak ho uklidí.
Použití wait:
...
#include <sys/wait.h>
...
int child_status;
...
if (child!=0) {
	wait(&child_status);
	if (WIFEXITED(child_status)) {
		printf("Child se ukončil normálně s kódem %d",WEXITSTATUS(child_status));
	} else {
		printf("Child se ukončil abnormálně");
	}
} else {
	printf("Child program ls:%d",(int)getpid());
...  
Jako u exec je wait příkazů několik např. wait3 poskytuje informace o využití CPU child procesem.

Tak to je k procesům asi všechno. Ale určitě se k nim ještě vrátíme v některé z dalších kapitol.

Signály

Signály jsou zprávy, které slouží ke komunikaci mezi procesy. Je jich několik (liší se na různých systémech). Nejpoužívanější jsou ale SIGUSR1 SIGUSR2 SIGTERM a SIGKILL. Pokud pošlete procesu signál, tak ho zpracuje ihned. Dokonce ani nezpracuje právě prováděný příkaz. Jediné, co můžeme dělat je zavést handler pro zpracování určitého signálu. Pak už si nemusíme ničeho všímat. Pár důležitých signálů: SIGUSR1 a SIGUSR2 jsou určené pro dorozumívání mezi procesy. SIGTERM je signál, který zdělí procesu, že by se měl ukončit. Tenhle signál může program odchytit a včas si zavřít všechny soubory a uvolnit paměť atd. SIGKILL už odchytit nelze a proces jednoduše zabije. Ještě se používá SIGHUP, který má probudit spící program (např. démona), nebo přimět program k znovupřečtění konfigurace. Signály si neposílají jen uživatelské procesy, ale některé posílá i kernel. Například SIGSEGV chyba segmentace, nebo SIGBUS chyba sběrnice. Podívejte se na $ kill -l a funkce signálů si myslím odvodíte sami ze jmen.

#include <stdio.h>
#include <signal.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>


sig_atomic_t sigcount=0;


void handler(int signum) {
	sigcount++;
}


int main() {
	struct sigaction sa;
	int oldval=0;
	memset(&sa,0,sizeof(sa));
	sa.sa_handler=&handler;
	sigaction(SIGUSR1,&sa,NULL);
	while (sigcount<10) {
		if (oldval!=sigcount) {
			printf("Signal raised");
			oldval=sigcount;
		}
	}
	printf("Done");
	return 0;
}  

Program můžete vyzkoušet tak, že budete z jiného terminálu posílat programu signál usr1 programu přes příkaz kill $ kill -SIGUSR1 (pid)Pro globální proměnné používejte typ sig_atomic_t. Přestože je tento typ jen jiným jménem pro int, je důležité ho používat, protože přiřazení hodnoty takovému typu se provede v jediné instrukci a nemůže být přerušeno jiným signálem. sa.sa_handler může obsahovat buď SIG_DFL, což definuje defaultní reakci na signál, SIG_IGN, což signál jednoduše ignoruje nebo ukazatel na funkci handleru. Funkce handleru musí být typu void a mít jeden parametr, který bude obsahovat číslo signálu. Funkce by měla obsahovat co nejmenší počet příkazů. Nejlépe jeden a to přiřazení. Proto by se mělo v hlavním programu kontrolovat, jestli byl signál obdržen a vše provést, tak jak je uvedeno v příkladě.

2006-12-06 | BOby
Reklamy:
„Na světě je zavedeno, že spousta hloupých si hraje na chytré. Z chytrých, kterých je na světě nedostatek, jen ti nejchytřejší si hrají na hloupé.“ Jan Werich