Procesy a IPC
Procesy
Proces je, zjednodušeně řečeno, spuštěný program. Proces se může nacházet v různých stavech, může běžet, může čekat až mu jádro přidělí procesor, může být blokovaný (například když čeká na vstup od uživatele), uspaný (třeba pomocí CTRL+Z nebo signálem) a nebo se z něj může stát zombie :-).
Už jste seznámili s funkcemi na spouštění programů.
Teď vám ukáži, jak se může běžící proces rozdvojit. Pomocí této skvělé
funkce můžete udělat z jednoho procesu dva. Druhý proces může s prvním
nějak komunikovat a spolupracovat, nebo může spustit jiný program
– tím se nasimuluje fungování funkce spawn()
, kterou
znáte z Windows.
Ke zdvojení procesu se používá funkce fork()
.
Protože má každý proces vlastní (virtuální) paměť, fork()
vytvoří
kompletní kopii procesu, tj. jak instrukce, tak přidělenou paměť a to včetně
té dynamicky přidělené!
Neudělá to ovšem hned, funguje to na principu tzv. copy on write,
tj pokud v novém procesu do paměti nic nezapíšete a rovnou spustíte pomocí
exec*()
nový program, k žádnému kopírování paměti nedojde.
Tak úžasně efektivně se Linux chová :-).
Pokud jde všechno hladce, tak poté, co se zavolá funkce fork()
se už nacházíte buď v původním procesu (tvz. rodiči), nebo v novém procesu (tzv. dítěti).
Kde jste poznáte podle návratové hodnoty funkce fork()
, kterou je
buď nenulová hodnota PID (process id) dítěte, pokud jste v rodiči,
nebo 0, pokud jste v dítěti.
V ukázce použiji program mudrc z kapitoly Spouštění programů.
V příkladu použiji ještě 2 šikovné funkce:
Na začátku programu uvedu hlavičkové soubory a definici cesty k programu mudrc.
- /*------------------------------------------------*/
- /* 22process/spawn.c */
- #include <sys/types.h>
- #include <sys/wait.h>
- #include <unistd.h>
- #include <stdlib.h> /* abort */
- #include <stdio.h>
- #include <stdbool.h>
- #define MUDRC "./mudrc"
Ve funkci spustProgram()
process rozdvojím.
V novém procesu (fpid == 0) spustím program mudrc.
Spuštěný program tento proces nahradí, tedy ukončí, zatímco rodičovský proces
pokračuje ve svém vykonávání.
- pid_t spustProgram() {
- pid_t fpid;
- fpid = fork();
- }
- /* child process */
- /* execl ukonci proces */
- }
- /*
- Tady pokracuje rodicovsky proces,
- dite (tj zkopirovany proces) jen v pripade selhani execl.
- */
- return fpid;
- }
Funkce main jen zavolá spustProgram()
a vypíše
nějaké informace. Řádek 41 až 45 si zatím nevšímejte.
- pid_t pid;
- pid = spustProgram();
- #if defined WAIT_ON_CHILD
- /* sem neco pozdeji doplnim */
- #elif defined ZOMBIE
- /* sem neco pozdeji doplnim */
- #endif
- }
Výstup z programu spawn může vypadat takto:
rodič: moje id = 8108 rodič: Číslo 8108 žije! rodič: Id potomka = 8109. dítě: Moje id = 8109 dítě: Rodičovské id = 8108 *************************** * mudrc rika: * * Argumenty: * * 1) Hello * * 2) World * ***************************
Všiměte si, že v tomto případě rodič vypsal své informace dříve, než se podařilo spustit program mudrc, proto je výstup z mudrce až na konci.
Výstup může ale vypadat i takto:
rodič: moje id = 8113 rodič: Číslo 8113 žije! rodič: Id potomka = 8114. dítě: Moje id = 8114 dítě: Rodičovské id = 1 *************************** * mudrc rika: * * Argumenty: * * 1) Hello * * 2) World * ***************************
Jakto že je rodičkovské id 1?
V Linuxu musí mít každý proces nějakého rodiče. Pokud rodič skončí dřív
než dítě (což se stalo v tomto případě), přiřadí oprační systém dítěti
jako rodiče process s id 1, což je proces init
, který se spouští
při startu operačního systému
jako první a jehož účelem je nastartovat ostatní procesy v Linuxu.
USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND
root 1 0.0 0.0 25084 2544 ? Ss srp13 0:37 /sbin/init showopts
Dokážete odhadnout výstup v případě, kdy se pokusíte spustit neexistující
program a execl()
funkce tak selže?
Čekání na potomka
Funkce waitpid()
vám umožní počkat na to, až potomek skončí.
pid je process id potomka, na kterého chcete počkat.
status je proměnná, do které se uloží některé informace
o procesu, které můžete zkoumat různými makry. Například
WIFEXITED(status)
vrací true, pokud potomek volal
funkci exit()
, WEXITSTATUS(status) vám vrátí argument
volané funkce exit()
atp. Kompletní seznam najdete,
jako vždy, v manuálových stránkách. Argument options
nějak ovlivňuje chování waitpid()
, ale to teď není důležité,
můžete ho nechat 0 :-).
Doplňte si do funkce main()
toto:
- #if defined WAIT_ON_CHILD
- int stat_loc;
- waitpid(pid, &stat_loc, 0);
- WEXITSTATUS(stat_loc));
- WTERMSIG(stat_loc));
- }
- }
- #elif defined ZOMBIE
- /* sem neco pozdeji doplnim */
- #endif
Takto část programu se zkompiluje, když definujete makro WAIT_ON_CHILD, např. při překladu:
Výstup pak bude vypadat nějak takto:
rodič: moje id = 8316 rodič: Číslo 8316 žije! rodič: Id potomka = 8317. dítě: Moje id = 8317 dítě: Rodičovské id = 8316 *************************** * mudrc rika: * * Argumenty: * * 1) Hello * * 2) World * *************************** rodič: Dítě volalo exit(0)
Zombie
Co se stane, když dítě zemře dříve než rodič? Stane se z něj ZOMBIE!
Aby mohl rodič získat informace o dítěti voláním waitpid()
,
musí zůstat dítě i po skončení stále v paměti. Abyste zabránili
zombiecalipse, měli byste zavolat waitpid()
pro každé dítě
(pokud rodič poběží ještě dlouho po té, co dítě skončilo). Tato funkce
si přečte potřebné informace ze zombíka (nejdřív si počká až dítě umře)
a zombie se odstraní.
Pokud nechcete kvůli dítěti čekat, můžete to udělat v novém vlákně (o kterých
bude řeč v některé z dalších kapitol).
Doplňte si do funkce main()
tuto poslední část.
Kód uspí rodiče na 30 vteřin, během kterých si budete moci prohlédnout pomocí
příkazu ps
dětský proces ve stavu <defunct>, tedy zombíka.
$ ./spawnzombie
rodič: moje id = 8840
rodič: Číslo 8840 žije!
rodič: Id potomka = 8841.
rodič: Spím 30 sekund.dítě: Moje id = 8841
dítě: Rodičovské id = 8840
***************************
* mudrc rika: *
* Argumenty: *
* 1) Hello *
* 2) World *
***************************
rodič: Spím na 28 sekund.
## V jiné konzoli:
$ ps axo ppid=,pid=,stat=,command= | egrep mudrc
8840 8841 Z+ [mudrc] <defunct>
Vidíte, parent process id 8840, process id 8841 i zombie stav (Z+, <defunct>) sedí :-).
Sdílená paměť (IPC)
IPC je zkratka pro Inter-process communication, tedy komunikace mezi procesy.
Jak už víte, když vytvoříte nový proces, zkopíruje se celá paměť původního procesu (včetně instrukcí procesu) a nový proces pracuje s touto kopií. Procesy jsou od sebe striktně oddělené a tak nemůže jeden zasáhnout do paměti druhého.
Nebylo by úžasné, kdyby si dva procesy mohli navzájem povídat? Představte si ty možnosti! Pokud máte víceprocesorový počítač, každý proces může běžet na jiném procesoru a přesto by mohli spolupracovat na řešení jednoho problému.
Jednou z možností IPC je posílání signálů. Můžte využít například signály SIGUSR1 a SIGUSR2. To je ale hodně omezený způsob komunikace.
Další možností IPC je, že procesy budou zapisovat data do souboru. Aby si nelezly procesy pod ruce, měli byste takové soubory zamykat. To už je slušný způsob, jak si vyměňovat data.
Je tu ale ještě něco lepšího. Sdílená paměť. Paměť, do které může zapisovat více procesů.
Práce se sdílenou pamětí vypadá asi takto:
- Získáte sdílenou paměť funkcí
shmget()
. Funkce vrátí ID sdílené paměti. - Pomocí
shmat()
získáte odkaz na zdílenou paměť. - Pracujete …
- Uvolníte odkaz pomocí
shmdt()
- Pokračujete bodem 1, dokud potřebujete.
- Odstraníte sdílenou paměť funkcí
shmctl()
Funkce shmget
vrací id sdílené paměti, která
se nachází pod klíčem key. Musíte uvést její velikost
v bajtech (size) a nějaké příznaky shmflg, jako například:
IPC_CREAT | Pokud sdílená paměť s tímto klíčem neexistuje, vytvoř ji. |
---|---|
IPC_EXCL | Pokud sdílená paměť s tímto klíčem existuje, skonči chybou EEXIST |
S_IRUSR | Uživatel bude mít právo čtení |
S_IWUSR | Uživatel bude mít právo zápisu |
Práva ke sdílené paměti se dají nastavovat stejně jako u funkce open()
.
Klíč je „libovolné“ kladné číslo, na kterém se procesy dohodnou,
aby měli přístup ke stejné sdílené paměti. Samozřejmě je tu riziko, že když
si dva programátoři vyberou stejný klíč, budou si navzájem sdílenou paměť
přepisovat. Když uvedete místo čísla konstantu IPC_PRIVATE
,
vybere pro vás operační systém nepoužitý klíč. To ukážu až v další kapitole.
Teď vám ukáži 4 programy, které budou pracovat s jednou sdílenou pamětí. Všechny využívají následující hlavičkový soubor, který definuje délku sdílené paměti a klíč ke sdílené paměti.
- /*------------------------------------------------*/
- /* 22process/shmap.h */
- #ifndef _SHMAP_H
- #define _SHMAP_H
- #define N 512
- #define SKEY 12345
- #endif
- /*------------------------------------------------*/
První program vytvoří sdílenou paměť, pokud už pod klíčem SKEY neexistuje. Pokud bude existovat už při prvním volání programu, zřejmě tuto sdílenou paměť využívá nějaký jiný program ve vašem počítači. V takovém případě si SKEY změňte na něco jiného.
Na sdílenou paměť získá odkaz pomocí volání shmat()
.
Druhý a třetí argument nechte vždy NULL a 0. Nejspíš je nebudete
nikdy potřebovat, v případě zájmu si je nastudujte z manuálové stránky.
Sdílenou paměť v programu vynuluji, abych při čtení textů nedostal nějaké nesmysly.
Nakonec se odkaz uvolní pomocí shmdt()
.
Tím se ale uvolní opravdu
jen odkaz, sdílená paměť zůstane v počítači „navždy“,
pokud ji neuvolníte voláním shmctl()
, viz další příklad, nebo
nerestartujete počítač.
- /*------------------------------------------------*/
- /* 22process/shmap_create.c */
- #include <sys/stat.h>
- #include <sys/ipc.h>
- #include <sys/shm.h>
- #include <stdio.h>
- #include <errno.h>
- #include <string.h>
- #include "shmap.h"
- IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR);
- }
- else {
- }
- }
- shmdt(pole);
- }
- /*------------------------------------------------*/
Segment s key = 12345 byl vytvořen.
$ ./shmap_create
Segment s key = 12345 už existuje.
Druhý program také získá ID paměti na základě klíče SKEY,
aby mohl použít ID k odstranění sdílené paměti pomocí shmctl()
.
Funkce shmctl()
se používá k provádění různých
příkazů (cmd) nad sdílenou pamětí. Odtranění
paměti je příkaz IPC_RMID
.
- /*------------------------------------------------*/
- /* 22process/shmap_destroy.c */
- #include <sys/stat.h>
- #include <sys/ipc.h>
- #include <sys/shm.h>
- #include <stdio.h>
- #include <errno.h>
- #include "shmap.h"
- int ret;
- S_IRUSR | S_IWUSR);
- }
- }
- ret = shmctl(segment_id, IPC_RMID, NULL);
- }
- }
- /*------------------------------------------------*/
Segment s key = 12345 už existuje.
$ ./shmap_destroy
Segment s key = 12345 byl smazán.
$ ./shmap_destroy
Segment s key = 12345 neexistuje.
Na dalším programu není nic nového, jen zapíše do sdílené paměti nějaký text, který mu předáte na příkazové řádce.
- /*------------------------------------------------*/
- /* 22process/shmap_use.c */
- #include <sys/stat.h>
- #include <sys/ipc.h>
- #include <sys/shm.h>
- #include <stdio.h>
- #include <errno.h>
- #include <getopt.h>
- #include <string.h>
- #include "shmap.h"
- int op;
- do {
- op = getopt(argc, argv, optstring);
- }
- }
- /* zajimava cast zacina zde: */
- S_IRUSR | S_IWUSR);
- }
- }
- shmdt(pole);
- }
- /*------------------------------------------------*/
Poslední program použije shmctl()
k získání
nějakých informací o sdílené paměti pomocí příkazu IPC_STAT
.
Informace se uloží do struktury struct shmid_ds
, jejíž odkaz
je třetím argumentem funkce shmctl()
. Co všechno struktura
obsahuje (například poslední čas použití paměti) se dočtete
v manuálové stránce.
Program taky načte a vypíše řetezec ze sdílené paměti.
- /*------------------------------------------------*/
- /* 22process/shmap_info.c */
- #include <sys/stat.h>
- #include <sys/ipc.h>
- #include <sys/shm.h>
- #include <stdio.h>
- #include <errno.h>
- #include "shmap.h"
- int ret;
- S_IRUSR | S_IWUSR);
- }
- }
- ret = shmctl(segment_id, IPC_STAT, &buf);
- }
- shmdt(pole);
- }
- /*------------------------------------------------*/
Kompletní ukázka použití programů může vypadat třeba takto:
Segment s key = 12345 byl vytvořen.
$ ./shmap_use -t "Hello World"
$ ./shmap_info
Key = 12345, Id = 188284966
Pole obsahuje 'Hello World'
$ ./shmap_destroy
Segment s key = 12345 byl smazán.
$ ./shmap_info
Segment s key = 12345 neexistuje.
Linuxové příkazy pro sdílenou paměť
V Linuxu existují příkazy ipcs
a ipcrm
,
které můžete použít pro zobrazení či smazání sdílené paměti.
Segment s key = 12345 byl vytvořen.
$ ./shmap_info
Key = 12345, Id = 18831773
$ $ ipcs -m
------ Segmenty sdílené paměti --------
klíč shmid vlastník práva bajty nattch stav
...
0x00003039 188317734 petr 600 512 0
...
$ ipcrm -M 12345 # nebo taky: ipcrm -M 0x00003039
$ ./shmap_info
Segment s key = 12345 neexistuje.
Ne že se teď vrhnete na programování a začnete využívat nově nabyté znalosti, musím vás pozastavit. Není dobré, aby ke sdílené paměti přistupovali dva programy najednou a měnili si data pod rukama. Přečtěte si další kapitolu ať víte, co tím myslím.