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á :-).

pid_t fork(void);

Pokud jde všechno hladce, tak ve chvíli kdy program zavolá funkci fork() už jste 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 fork(), kterou je PID (process id) dítěte, 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:

#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void); /* vrati ID procesu */
pid_t getppid(void); /* vrati ID rodice (parent process) */

Na začátku programu uvedu hlavičkové soubory a definici cesty k programu mudrc.

  1. /*------------------------------------------------*/
  2. /* 22process/spawn.c                              */
  3.  
  4. #include <sys/types.h>
  5. #include <sys/types.h>
  6. #include <sys/wait.h>
  7. #include <unistd.h>
  8. #include <stdlib.h> /* abort */
  9. #include <stdio.h>
  10. #include <stdbool.h>
  11.  
  12. #define MUDRC "./mudrc"
  13.  

Ve funkci spustProgram() process rozdvojím. V dítěti (fpid == 0) spustím program mudrc. Spuštěný program proces dítěte nahradí, tedy ukončí, ale rodičovský proces poběží dále.

  1. pid_t spustProgram() {
  2.     pid_t fpid;
  3.     fpid = fork();
  4.    
  5.     if(fpid == -1) {
  6.         perror(NULL);
  7.          abort();
  8.     }
  9.     else if(fpid == 0) {
  10.         /* child process */
  11.         printf("dítě: Moje id = %d\n", getpid());
  12.         printf("dítě: Rodičovské id = %d\n", getppid());
  13.         /* execl ukonci proces */
  14.         execl(MUDRC, MUDRC,"Hello", "World", (char *)NULL);
  15.         perror("rodič: Program se nepodařilo spustit\n");
  16.     }
  17.     return fpid;
  18. }
  19.  

Funkce main jen zavolá spustProgram() a vypíše nějaké informace. Řádek 41 až 45 si zatím nevšímejte.

  1. int main(void) {
  2.     pid_t pid;
  3.    
  4.     printf("rodič: moje id = %d\n", getpid());
  5.     pid = spustProgram();
  6.     printf("rodič: Číslo %d žije!\n", getpid());
  7.     printf("rodič: Id potomka = %d.\n", pid);
  8.  
  9. #if defined WAIT_ON_CHILD
  10.         /* sem neco pozdeji doplnim */
  11. #elif defined ZOMBIE
  12.         /* sem neco pozdeji doplnim */
  13. #endif
  14.  
  15.     return 0;
  16. }

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.

$ ps u -p 1
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

Čekání na potomka

Funkce waitpid() vám umožní počkat na to, až potomek skončí.

pid_t waitpid(pid_t pid, int *status, int options);

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:

  1. #if defined WAIT_ON_CHILD
  2.     int stat_loc;
  3.     if(pid > 0) { /* spusti se jen v rodici */
  4.         waitpid(pid, &stat_loc, 0);
  5.         if(WIFEXITED(stat_loc)) {
  6.             printf("rodič: Dítě volalo exit(%i)\n",
  7.                  WEXITSTATUS(stat_loc));
  8.         } else if(WIFSIGNALED(stat_loc)) {
  9.             printf("rodič: Dítě dostalo signál %d\n",
  10.                  WTERMSIG(stat_loc));
  11.         }
  12.     }
  13. #elif defined ZOMBIE
  14.         /* sem neco pozdeji doplnim */
  15. #endif

Takto část programu se zkompiluje, když definujete makro WAIT_ON_CHILD, např. při překladu:

$ clang -g -Wall -o spawnwait spawn.c -DWAIT_ON_CHILD

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.

  1. #elif defined ZOMBIE
  2.     int sec = 30;
  3.     printf("rodič: Spím %d sekund.", sec);
  4.     fflush(stdout);
  5.     while(sec--) {
  6.         fflush(stdout);
  7.         printf("\rrodič: Spím na %d sekund.", sec);
  8.         sleep(1);
  9.     }
  10.     printf("\n");
  11. #endif

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.

$ gcc -g -Wall -o spawnzombie spawn.c -DZOMBIE
$ ./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ém okně:
$ 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 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:

  1. Získáte sdílenou paměť funkcí shmget(). Funkce vrátí ID sdílené paměti.
  2. Pomocí shmat() získáte odkaz na zdílenou paměť.
  3. Pracujete …
  4. Uvolníte odkaz pomocí shmdt()
  5. Pokračujete bodem 1, dokud potřebujete.
  6. Odstraníte sdílenou paměť funkcí shmctl()
int shmget(key_t key, size_t size, int shmflg);

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_CREATPokud sdílená paměť s tímto klíčem neexistuje, vytvoř ji.
IPC_EXCLPokud sdílená paměť s tímto klíčem existuje, skonči chybou EEXIST
S_IRUSRUživatel bude mít právo čtení
S_IWUSRUž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.

  1. /*------------------------------------------------*/
  2. /* 22process/shmap_create.c                       */
  3. #ifndef _SHMAP_H
  4. #define _SHMAP_H
  5.  
  6. #define N  512
  7. #define SKEY 12345
  8.  
  9. #endif
  10. /*------------------------------------------------*/

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.

void *shmat(int shmid, const void *shmaddr, int shmflg);

Sdílenou paměť v programu vynuluji, abych při čtení textů nedostal nějaké nesmysly.

Nakonec se odkaz uvolní pomocí shmdt().

int shmdt(const void *shmaddr);

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č.

  1. /*------------------------------------------------*/
  2. /* 22process/shmap_create.c                       */
  3.  
  4. #include <sys/stat.h>
  5. #include <sys/ipc.h>
  6. #include <sys/shm.h>
  7. #include <stdio.h>
  8. #include <errno.h>
  9. #include <string.h>
  10. #include "shmap.h"
  11.  
  12. int main(void) {
  13.     int segment_id; /* identifikace zdilene pameti */
  14.     char * pole;
  15.  
  16.     segment_id = shmget(SKEY, sizeof(char)*N,
  17.              IPC_CREAT | IPC_EXCL | S_IRUSR | S_IWUSR);
  18.     if(segment_id < 0) {
  19.         if(errno == EEXIST) {
  20.             fprintf(stderr,"Segment s key = %i už existuje.\n", SKEY);
  21.             return 1;
  22.         }
  23.         else {
  24.             fprintf(stderr,"Stala se nějaká chyba\n");
  25.             return 1;
  26.         }
  27.     }
  28.     printf("Segment s key = %i byl vytvořen.\n", SKEY);
  29.  
  30.     pole = (char *) shmat(segment_id, NULL, 0);
  31.     memset(pole, '\0', N);
  32.     shmdt(pole);
  33.     return 0;
  34. }
  35.  
  36. /*------------------------------------------------*/
$ ./shmap_create
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().

int shmctl(int shmid, int cmd, struct shmid_ds *buf);

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.

  1. /*------------------------------------------------*/
  2. /* 22process/shmap_destroy.c                      */
  3.  
  4. #include <sys/stat.h>
  5. #include <sys/ipc.h>
  6. #include <sys/shm.h>
  7. #include <stdio.h>
  8. #include <errno.h>
  9. #include "shmap.h"
  10.  
  11. int main(void) {
  12.     int segment_id; /* identifikace zdilene pameti     */
  13.     int ret;
  14.  
  15.     segment_id = shmget(SKEY, sizeof(char)*N,
  16.              S_IRUSR | S_IWUSR);
  17.     if(segment_id < 0) {
  18.         if(errno == ENOENT) {
  19.             fprintf(stderr,"Segment s key = %i neexistuje.\n", SKEY);
  20.             return 0;
  21.         }
  22.         fprintf(stderr,"Nějaká chyba získávání segmentu.");
  23.         return 1;
  24.     }
  25.     ret = shmctl(segment_id, IPC_RMID, NULL);
  26.     if(ret < 0) {
  27.         fprintf(stderr,"Nějaká chyba mazání segmentu.");
  28.         return 1;
  29.     }
  30.     fprintf(stderr,"Segment s key = %i byl smazán.\n", SKEY);
  31.     return 0;
  32. }
  33.  
  34. /*------------------------------------------------*/
$ ./shmap_create
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.

  1. /*------------------------------------------------*/
  2. /* 22process/shmap_use.c                          */
  3.  
  4. #include <sys/stat.h>
  5. #include <sys/ipc.h>
  6. #include <sys/shm.h>
  7. #include <stdio.h>
  8. #include <errno.h>
  9. #include <getopt.h>
  10. #include <string.h>
  11. #include "shmap.h"
  12.  
  13. int main(int argc, char *argv[]) {
  14.     int segment_id; /* identifikace zdilene pameti     */
  15.     char *pole;
  16.     char *retezec = NULL;
  17.     char *optstring ="+t:";
  18.     int op;
  19.     do {
  20.         op = getopt(argc, argv, optstring);
  21.         switch(op) {
  22.             case -1:  break;
  23.             case '?': /* getopt vypise chybu*/ break;
  24.             case 't': retezec = optarg; break;
  25.         }
  26.     } while (op != -1);
  27.  
  28.     if(retezec == NULL) {
  29.         fprintf(stderr,"Užití: %s -t \"retezec\"\n", argv[0]);
  30.         return 1;
  31.     }
  32.     /* zajimava cast zacina zde: */
  33.     segment_id = shmget(SKEY, sizeof(char)*N,
  34.              S_IRUSR | S_IWUSR);
  35.     if(segment_id < 0) {
  36.         if(errno == ENOENT) {
  37.             fprintf(stderr,"Segment s key = %i neexistuje.\n", SKEY);
  38.             return 1;
  39.         }
  40.         fprintf(stderr, "Něco je špatně.\n");
  41.         return 1;
  42.     }
  43.     pole = (char *) shmat(segment_id, NULL, 0);
  44.     strncpy(pole, retezec, N-1);
  45.     shmdt(pole);
  46.  
  47.     return 0;
  48. }
  49.  
  50. /*------------------------------------------------*/

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.

  1. /*------------------------------------------------*/
  2. /* 22process/shmap_info.c                         */
  3.  
  4. #include <sys/stat.h>
  5. #include <sys/ipc.h>
  6. #include <sys/shm.h>
  7. #include <stdio.h>
  8. #include <errno.h>
  9. #include "shmap.h"
  10.  
  11. int main(void) {
  12.     int segment_id; /* identifikace zdilene pameti     */
  13.     struct shmid_ds buf; /* informace o zdilene pameti */
  14.     char *pole;
  15.     int ret;
  16.  
  17.     segment_id = shmget(SKEY, sizeof(char)*N,
  18.              S_IRUSR | S_IWUSR);
  19.     if(segment_id < 0) {
  20.         if(errno == ENOENT) {
  21.             fprintf(stderr,"Segment s key = %i neexistuje.\n", SKEY);
  22.             return 1;
  23.         }
  24.         fprintf(stderr, "Něco je špatně.\n");
  25.         return 1;
  26.     }
  27.     ret = shmctl(segment_id, IPC_STAT, &buf);
  28.     if(ret < 0) {
  29.         fprintf(stderr, "Něco je špatně. Žeby přístupová práva?\n");
  30.         return 1;
  31.     }
  32.    
  33.     printf("Key = %i, Id = %i\n", buf.shm_perm.__key, segment_id);
  34.     pole = (char *) shmat(segment_id, NULL, 0);
  35.     printf("Pole obsahuje '%s'\n", pole);
  36.     shmdt(pole);
  37.  
  38.     return 0;
  39. }
  40.  
  41. /*------------------------------------------------*/

Kompletní ukázka použití programů může vypadat třeba takto:

$ ./shmap_create
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.

$ ./shmap_create
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.

Komentář Hlášení chyby
Vytvořeno: 26.8.2014
Naposledy upraveno: 23.9.2017
Tato stánka používá ke svému běhu cookies, díky kterým je možné monitorovat, co tu provádíte (ne že bych to bez cookies nezvládl). Také vás tu bude špehovat google analytics. Jestli si myslíte, že je to problém, vypněte si cookies ve vašem prohlížeči, nebo odejděte a už se nevracejte :-). Prohlížením tohoto webu souhlasíte s používáním cookies. Dozvědět se více..