Struktury danych do implementacji IPC

Błażej Osiński

IPC, czyli Interprocess Communications, to zbiór mechanizmów służących do wymiany informacji między procesami. Elementami zaimplementowanego w Linuxie systemu SysV IPC są semafory, kolejki komunikatów i współdzielone segmenty pamięci.

Struktury danych służące do implementacji tych mechanizmów zmieniały się w kolejnych wersjach jądra, by zapewnić obsługę SMP, przestrzeni nazw, a także zwiększyć efektywność dzięki RCU.

Używanie IPC...

...opanowaliśmy na kursowym wykładzie z SO. Przypomnę tylko, że mieliśmy funkcje ***get(), ***ctl(), gdzie pod *** ukrywa się jeden ze skrótów: sem, msq, shm.

Klucze i identyfikatory

Zwykle w celu stworzenia nowego zasobu IPC korzystamy z klucza IPC. Jeżeli chcemy, by dwa procesy współdzieliły ze sobą zasób IPC, to będą musiały znać ten wspólny klucz. Istnieje też ryzyko konfliktu, gdy jakiś inny proces będzie próbował wykorzystać ten sam klucz.

Do rozpoznawania przydzielonych zasobów IPC służą ich identyfikatory. Są one unikatowe w ramach jednej przestrzeni nazw. Oblicza je funkcja:


    inline int ipc_buildid(int id, int seq)
    {
      return SEQ_MULTIPLIER * seq + id;
    }
   

Zapewnia nam ona, że identyfikatory nie będą się powtarzać zbyt często. Zmienna seq jest zwiększana przy każdej dealokacji zasobu.

Struktury wspólne dla wszystkich typów zasobów

Informacje o każdym zasobie IPC, niezależnie od typu, są przechowywane w strukturze kern_ipc_perm


  struct kern_ipc_perm
  {
          spinlock_t      lock;
          int             deleted;
          int             id;
          key_t           key;
          uid_t           uid;  /* właściciela */
          gid_t           gid;
          uid_t           cuid; /* twórcy */
          gid_t           cgid;
          mode_t          mode; /* Maska bitowa uprawnień */ 
          unsigned long   seq;
          void            *security;
  };
  

Kiedyś (do wersji 2.2 włącznie) dla każdego typu zasobu trzymano globalną statyczną tablicę wskaźników na struktury typu kern_ipc_perm (które wtedy nazywały się ipc_perm). W jądrze 2.4 tablica była dynamiczna - w czasie działania systemu jej rozmiar mógł się zmieniać. Obecnie wprowadzony już został podział na przestrzenie nazw, zatem nie ma globalnych struktur przechowujących informacje o zasobach IPC. Zamiast nich, dla każdej przestrzeni nazw, w strukturze ipc_namespace mamy pole struct ipc_ids ids[3];, które dla każdego typu zasobów trzyma następujące informacje:


  struct ipc_ids {
          int in_use;
          unsigned short seq;
          unsigned short seq_max;
          struct rw_semaphore rw_mutex;
          struct idr ipcs_idr;
  };
  

Pole ipcs_idr to drzewo pozycyjne, które przechowuje mapowanie między identyfikatorami zasobów, a wskaźnikami do obiektów typu kern_ipc_perm. W porównaniu do tablicy, którą trzeba było liniowo przeszukiwać, zyskaliśmy oszczędność czasu.

Semafory

Przypomnijmy, że semafory IPC składają się z pewnej liczby semaforów prostych. Do przechowywania informacji o całej grupie służy struktura:


  /* One sem_array data structure for each set of semaphores in the system. */
  struct sem_array {
          struct kern_ipc_perm    sem_perm;       /* permissions .. see ipc.h */
          time_t                  sem_otime;      /* last semop time */
          time_t                  sem_ctime;      /* last change time */
          struct sem              *sem_base;      /* ptr to first semaphore in array */
          struct list_head        sem_pending;    /* pending operations to be processed */
          struct list_head        list_id;        /* undo requests on this array */
          int                     sem_nsems;      /* no. of semaphores in array */
          int                     complex_count;  /* pending complex operations */
  };
  

Procesy, które zawisły na semaforze znajdują się w kolejce sem_pending w strukturze typu:


  /* One queue for each sleeping process in the system. */
  struct sem_queue {
          struct list_head        simple_list; /* queue of pending operations */
          struct list_head        list;    /* queue of pending operations */
          struct task_struct      *sleeper; /* this process */
          struct sem_undo         *undo;   /* undo structure */
          int                     pid;     /* process id of requesting process */
          int                     status;  /* completion status of operation */
          struct sembuf           *sops;   /* array of pending operations */
          int                     nsops;   /* number of operations */
          int                     alter;   /* does the operation alter the array? */
  };
  

Zwróćmy uwagę, że powyższa klasa ma 2 pola opisane jako "queue of pending operations" i jedno jako "array of pending operations". O co tu chodzi?

Undo

Operacje na semaforach mogą być wykonane w trybie undo. Wówczas, gdy proces umrze, to zostaną one automatycznie odwołane (o ile to możliwe). Jest to prosty sposób zapobiegania zakleszczeniom, gdy jakiś proces niespodziewanie zginie.


  struct sem_undo {
          struct list_head        list_proc;      /* per-process list: all undos from one process. */
                                                  /* rcu protected */
          struct rcu_head         rcu;            /* rcu struct for sem_undo() */
          struct sem_undo_list    *ulp;           /* sem_undo_list for the process */
          struct list_head        list_id;        /* per semaphore array list: all undos for one array */
          int                     semid;          /* semaphore set identifier */
          short *                 semadj;         /* array of adjustments, one per semaphore */
  };
  

Elementy sem_undo są chronione za pomocą rcu, co powinno polepszać efektywność.

Właściwa funkcjonalność jest realizowana bardzo prosto: w polu semadj znajduje się tablica liczb, o które należy zmienić wartości wszystkich semaforów prostych.

Zwróćmy uwagę, że struktury sem_undo leżą jednocześnie na dwóch listach: jednej właściwej dla procesu (list_proc) i drugiej dla semafora (list_id).

W zrozumieniu zależności między tymi strukturami może pomóc poniższy schemat:

Szkic zależności między strukturami danych dotyczących semafora.
Schemat zależności między strukturami danych dotyczących semafora.

Kolejki komunikatów

Kolejki komunikatów służą do przekazywania pewnych wiadomości między procesami. Z każdą wiadomością związany jest jej typ (liczba naturalna), możliwe jest selektywne pobieranie wiadomości np. tylko określonego typu. Z tego powodu nazwa kolejka nie odpowiada w pełni rzeczywistości - wiadomości nie zawsze są przekazywane w kolejności FIFO.

Do przesyłania i odbierania komunikatów służą operacje msgsnd i msgrcv.

Struktura przechowująca informacje o całej kolejce:


  /* one msq_queue structure for each present queue on the system */
  struct msg_queue {
          struct kern_ipc_perm q_perm;
          time_t q_stime;                 /* last msgsnd time */
          time_t q_rtime;                 /* last msgrcv time */
          time_t q_ctime;                 /* last change time */
          unsigned long q_cbytes;         /* current number of bytes on queue */
          unsigned long q_qnum;           /* number of messages in queue */
          unsigned long q_qbytes;         /* max number of bytes on queue */
          pid_t q_lspid;                  /* pid of last msgsnd */
          pid_t q_lrpid;                  /* last receive pid */
  
          struct list_head q_messages;
          struct list_head q_receivers;
          struct list_head q_senders;
  };
  

q_messages to głowa listy zawierającej komunikaty w strukturach:


  /* one msg_msg structure for each message */
  struct msg_msg {
          struct list_head m_list; 
          long  m_type;          
          int m_ts;           /* message text size */
          struct msg_msgseg* next;
          void *security;
          /* the actual message follows immediately */
  };
  

W strukturze tej nie ma specjalnego pola na dane. Następują one w pamięci bezpośrednio po tej strukturze. Maksymalna ilość danych, określona przez makro DATALEN_MSG, jest dobrana tak, by wraz ze strukturą msg_msg zajmować jedną stronę. Jeżeli komunikat jest dłuższy, to pozostałą część danych przechowuje jednokierunkowa lista buforów typu:


   struct msg_msgseg {
           struct msg_msgseg* next;
           /* the next part of the message follows immediately */
   };
   

Tak jak poprzednio właściwe dane znajdują się w pamięci tuż za strukturą msg_msgseg.

Na poniższym schemacie przedstawiono struktury danych do przechowywania kolejki złożonej z trzech komunikatów, z których jeden jest dość długi.

Schemat kolejki
Schemat prezentujący komunikaty w kolejce IPC.

Dość ciekawe wydaje mi się, że komunikaty nadal są przechowywane w postaci zwykłej listy. Gdy proces chce znaleźć komunikat określonego typu może być zmuszonydo przejrzenia całej kolejki. Asymptotyczne przyśpieszenie tego procesu, przy założeniu że proces musi otrzymać zawsze pierwszy z akceptowanych przez siebie komunikatów nie byłoby proste. Wydaje mi się, że można by je zrealizować z pomocą drzewa przedziałowego. Prawdopodobnie w praktyce byłoby to jednak wolniejsze rozwiązanie.

Czekające procesy

Są dwie sytuacje związane z obsługą kolejek komunikatów, w których procesy muszą usnąć:


  /* one msg_sender for each sleeping sender */
  struct msg_sender {
          struct list_head        list;
          struct task_struct      *tsk;
  };
  

  /*
   * one msg_receiver structure for each sleeping receiver:
   */
  struct msg_receiver {
          struct list_head        r_list;
          struct task_struct      *r_tsk;
  
          int                     r_mode;
          long                    r_msgtype;
          long                    r_maxsize;
  
          struct msg_msg          *volatile r_msg;
  };
  

Pamięć współdzielona

Dzięki zasobom tego typu procesy mogą w prosty sposób współdzielić ze sobą pewne segmenty pamięci.

Aby skorzystać z segmentu nie wystarczy go stworzyć. Proces musi go jeszcze dołączyć do swojej przestrzeni adresowej za pomocą funkcji shmat. Do odłączania służy funkcja shmdt.

W przeciwieństwie do innych zasobów IPC możemy zapewnić, że współdzielony segment pamięci zostanie usunięty po odłączeniu od niego ostatniego procesu. W tym celu wystarczy stworzyć go z ustawioną flagą SHM_DEST.

Struktura przechowująca informacje o współdzielonym segmencie pamięci:


  struct shmid_kernel /* private to the kernel */
  {       
          struct kern_ipc_perm    shm_perm;
          struct file *           shm_file;
          unsigned long           shm_nattch;
          unsigned long           shm_segsz;
          time_t                  shm_atim; /* czas ostatniego dołączenia, odłączenia i stworzenia */
          time_t                  shm_dtim;
          time_t                  shm_ctim;
          pid_t                   shm_cprid; /* pidy twórcy i ostatniego użytkownika */
          pid_t                   shm_lprid;
          struct user_struct      *mlock_user; /* użytkownik, który trzyma lock'a */
  };
  

Segmenty pamięci współdzielonej są przechowywane w systemie jako pliki. Można je znaleźć w katalogu /proc/pid/maps, a ich nazwy są postaci SYSVkey.

Do tego pliku odnosi się pole shm_file w powyższej strukturze.

Powstały plik jest mapowany do pamięci procesu gdy wywołana zostaje funkcja shmat. Tak jak w przypadku mapowania zwykłych plików wykorzystuje ona funkcję do_mmap.

Pliki segmentów pamięci bardzo niewiele różnią się od innych plików. Mają jedynie inne pewne funkcje z vm_operations_struct (np. shm_open).

Wymagane dodatkowe informacje o takich plikach są przechowywane w strukturze:


  struct shm_file_data {
          int id;
          struct ipc_namespace *ns;
          struct file *file;
          const struct vm_operations_struct *vm_ops;
  };
  

Wskazuje na nią pole private_data. Dane uzyskuje się poniższym makrem:


  #define shm_file_data(file) (*((struct shm_file_data **)&(file)->private_data))
  

Odnośniki