Search     or:     and:
 LINUX 
 Language 
 Kernel 
 Package 
 Book 
 Test 
 OS 
 Forum 
 iakovlev.org 
 Languages
 С
 GNU С Library 
 Qt 
 STL 
 Threads 
 C++ 
 Samples 
 stanford.edu 
 ANSI C
 Libs
 LD
 Socket
 Pusher
=> Pipes
 Encryption
 Plugin
 Inter-Process
 Errors
 Deep C Secrets
 C + UNIX
 Linked Lists / Trees
 Asm
 Perl
 Python
 Shell
 Erlang
 Go
 Rust
 Алгоритмы
NEWS
Последние статьи :
  Тренажёр 16.01   
  Эльбрус 05.12   
  Алгоритмы 12.04   
  Rust 07.11   
  Go 25.12   
  EXT4 10.11   
  FS benchmark 15.09   
  Сетунь 23.07   
  Trees 25.06   
  Apache 03.02   
 
TOP 20
 MINIX...3057 
 Solaris...2933 
 LD...2904 
 Linux Kernel 2.6...2470 
 William Gropp...2180 
 Rodriguez 6...2011 
 C++ Templates 3...1945 
 Trees...1936 
 Kamran Husain...1865 
 Secure Programming for Li...1791 
 Максвелл 5...1710 
 DevFS...1693 
 Part 3...1682 
 Stein-MacEachern-> Час...1632 
 Go Web ...1624 
 Ethreal 4...1618 
 Arrays...1606 
 Стивенс 9...1603 
 Максвелл 1...1592 
 FAQ...1538 
 
  01.01.2024 : 3621733 посещений 

iakovlev.org

Unix Multi-Process Programming Inter-Process Communications (IPC)

Примеры лежат тут (20 кб) . Одной из сильных сторон юникса является способность выполнять несколько процессов одновременно . Любая нетривиальная система , разработанная в линуксе , рано или поздно сталкивается с проблемой создания многопроцессорного приложения .

Что такое процесс в юниксе .

Процесс - экземпляр запущенной программы , который имеет свой собственный стек , набор страниц памяти , таблицу файловых дескрипторов и уникальный ID. Процесс может быть порожден несколько раз одной и той же программой , например если функция форкает процесс , и вызов этой функции рекурсивный . Понятно , что если программа имеет глобальную переменную "i" , и если эта программа порождает 2 процесса , то каждый процесс будет иметь свою копию этой переменной. Но с другой стороны , если эти 2 процесса попытаются сделать запись в один и тот же файл , ничего хорошего из этого не выйдет . Такие вещи нужо предотвращать с помощью механизма блокировки . Примером такого механизма являются семафоры .

Создание процесса

fork() System Call

fork - основной вариант для создания процесса . Уникальность fork в том , что этот системный вызов дважды (!) возвращает значение при вызове . При этом родительский процесс создает дочерний процесс и страницы памяти родительского процесса дублируются . В родителском процессе fork() возвращает ID дочернего процесса , в дочернем процессе fork() возвращает 0 . Если при fork() возникает ошибка , то возвращается -1 . Рассмотрим пример порождения процесса , который печатает "hello world" :
 #include <unistd.h>	/* defines fork(), and pid_t.      */
 #include <sys/wait.h>	/* defines the wait() system call. */
 /* storage place for the pid of the child process, and its exit status. */
 pid_t child_pid;
 int child_status;
 /* lets fork off a child process... */
 child_pid = fork();
 /* check what the fork() call actually did */
 switch (child_pid) {
     case -1:	/* fork() failed */
 	perror("fork");	/* print a system-defined error message */
 	exit(1);
     case 0:	/* fork() succeeded, we're inside the child process */
 	printf("hello world\n");
 	exit(0);	/* here the CHILD process exits, not the parent. */
     default:	/* fork() succeeded, we're inside the parent process */
 	wait(&child_status);	/* wait till the child process exits */
 }
 /* parent's process code may continue here... */
 

Child Process Termination

При выходе дочернего процесса последний сначала посылает сигнал родительскому процессу и только после этого удаляется из памяти . Если родительский процесс внезапно выходит и остаются его дочерние процессы , они становятся дочерними процессами у основного системного процесса "init" с ID=1 . Говорят , что они остаются в состоянии зомби . Их можно выявить с помощью команды "ps"

wait() System Call

Чтобы из родительского процесса узнать о состоянии его дочерних процессов , служит wait() system call. При его вызове ловится событие exit() дочернего процеса , если оно происходит .

Асинхронное сообщение от дочернего процесса

Когда дочерний процесс умирает , он посылает сигнал SIGCHLD (или SIGCLD) родительскому процессу . Родительский процес получает это сообщение асинхронно , и при вызове wait(), выясняется , что имеется зомби . Следующий пример показывает аналог "hello world" с использованием сигнального хэндлера :
 #include <stdio.h>     /* basic I/O routines.   */
 #include <unistd.h>    /* define fork(), etc.   */
 #include <sys/types.h> /* define pid_t, etc.    */
 #include <sys/wait.h>  /* define wait(), etc.   */
 #include <signal.h>    /* define signal(), etc. */
 /* first, here is the code for the signal handler */
 void catch_child(int sig_num)
 {
     /* when we get here, we know there's a zombie child waiting */
     int child_status;
     wait(&child_status);
     printf("child exited.\n");
 }
 /* and somewhere in the main() function ... */
 /* define the signal handler for the CHLD signal */
 signal(SIGCHLD, catch_child);
 /* and the child process forking code... */
 {
     int child_pid;
     int i;
     child_pid = fork();
     switch (child_pid) {
         case -1:         /* fork() failed */
             perror("fork");
             exit(1);
         case 0:          /* inside child process  */
             printf("hello world\n");
             sleep(5);    /* sleep a little, so we'll have */
                          /* time to see what is going on  */
             exit(0);
         default:         /* inside parent process */
             break;
     }
     /* parent process goes on, minding its own business... */
     /* for example, some output...                         */
     for (i=0; i<10; i++) {
         printf("%d\n", i);
         sleep(1);    /* sleep for a second, so we'll have time to see the mix */
     }
 }

Пайпы

С помощью пайпов процессы могут взаимодействовать друг с другом . Пайп - механизм , с помощью которого один процесс может послать байтовый поток данных другому процессу . Этот поток может быть взаимным и организован с помощью 2-х пайпов . Порядок , в котором байты пишутся в пайп , тот же самый , в котором они и читаются .

pipe() System Call

С помощью этого системного вызова создается пайп , который может быть использован в дальнейшем. Вызов имеет 2 аргумента , представляющих из себя файловые дескрипторы : один на чтение и другой на запись . Пример :
 /* first, define an array to store the two file descriptors */
 int pipes[2];
 /* now, create the pipe */
 int rc = pipe(pipes);
 if (rc == -1) { /* pipe() failed */
     perror("pipe");
     exit(1);
 }
После создания пайпа мы форкаем дочерний процесс , и в результате 2 файловых дескриптора из памяти родительского процесса копируются в память дочернего . Также копируется таблица файловых дескрипторов . В следующем примере родительский процесс читает данные от пользователя и посылает их дочернему процессу с помощью пайпа , где каждый байт пайпа соответствует символу , набранному пользователем :
 #include <stdio.h>    /* standard I/O routines.                */
 #include <unistd.h>   /* defines pipe(), amongst other things. */
 /* this routine handles the work of the child process. */
 void do_child(int data_pipe[]) {
     int c;	/* data received from the parent. */
     int rc;	/* return status of read().       */
     /* first, close the un-needed write-part of the pipe. */
     close(data_pipe[1]);
     /* now enter a loop of reading data from the pipe, and printing it */
     while ((rc = read(data_pipe[0], &c, 1)) > 0) {
 	putchar(c);
     }
     /* probably pipe was broken, or got EOF via the pipe. */
     exit(0);
 }
 /* this routine handles the work of the parent process. */
 void do_parent(int data_pipe[])
 {
     int c;	/* data received from the user. */
     int rc;	/* return status of getchar().  */
     /* first, close the un-needed read-part of the pipe. */
     close(data_pipe[0]);
     /* now enter a loop of read user input, and writing it to the pipe. */
     while ((c = getchar()) > 0) {
 	/* write the character to the pipe. */
         rc = write(data_pipe[1], &c, 1);
 	if (rc == -1) { /* write failed - notify the user and exit */
 	    perror("Parent: write");
 	    close(data_pipe[1]);
 	    exit(1);
         }
     }
     /* probably got EOF from the user. */
     close(data_pipe[1]); /* close the pipe, to let the child know we're done. */
     exit(0);
 }
 /* and the main function. */
 int main(int argc, char* argv[])
 {
     int data_pipe[2]; /* an array to store the file descriptors of the pipe. */
     int pid;       /* pid of child process, or 0, as returned via fork.    */
     int rc;        /* stores return values of various routines.            */
 
     /* first, create a pipe. */
     rc = pipe(data_pipe);
     if (rc == -1) {
 	perror("pipe");
 	exit(1);
     }
     /* now fork off a child process, and set their handling routines. */
     pid = fork();
     switch (pid) {
 	case -1:	/* fork failed. */
 	    perror("fork");
 	    exit(1);
 	case 0:		/* inside child process.  */
 	    do_child(data_pipe);
 	    /* NOT REACHED */
 	default:	/* inside parent process. */
 	    do_parent(data_pipe);
 	    /* NOT REACHED */
     }
     return 0;	/* NOT REACHED */
 }
Здесь в родительском процессе закрывается дескриптор пайпа на чтение , в дочернем процессе закрывается дескриптор пайпа на запись . Полностью код можно найти в архиве в файле one-way-pipe.c

2-направленное взаимодействие пайпов

Основная проблема при написании 2-направленных пайпов - проблем 'deadlock' , когда процесс подвисает на неопределенное время в ожидании данных из пайпа . Такая ситуация может возникнуть в следующих вариантах :
  • Оба пайпа пусты , а процессы ожидают данные .
  • Пайп имеет ограниченный буфер . При переполнении буфера пайп автоматически блокируется до тех пор , пока данные не будут прочитаны из этого буфера . Блокировка возникает также , если одновременно в оба пайпа происходит запись .
  • В следующем примере первый процесс посылает пайп второму процессу , который переводит символы в нижний регистр и посылает их назад первому процессу :
     #include <stdio.h>    /* standard I/O routines.                  */
     #include <unistd.h>   /* defines pipe(), amongst other things.   */
     #include <ctype.h>    /* defines isascii(), toupper(), and other */
                           /* character manipulation routines.        */
     /* function executed by the user-interacting process. */
     void user_handler(int input_pipe[], int output_pipe[])
     {
         int c;    /* user input - must be 'int', to recognize EOF (= -1). */
         char ch;  /* the same - as a char. */
         int rc;   /* return values of functions. */
         /* first, close unnecessary file descriptors */
         close(input_pipe[1]); /* we don't need to write to this pipe.  */
         close(output_pipe[0]); /* we don't need to read from this pipe. */
         /* loop: read input, send via one pipe, read via other */
         /* pipe, and write to stdout. exit on EOF from user.   */
         while ((c = getchar()) > 0) {
             /* note - when we 'read' and 'write', we must deal with a char, */
             /* rather then an int, because an int is longer then a char,    */
             /* and writing only one byte from it, will lead to unexpected   */
             /* results, depending on how an int is stored on the system.    */
             ch = (char)c;
     	/* write to translator */
             rc = write(output_pipe[1], &ch, 1);
     	if (rc == -1) { /* write failed - notify the user and exit. */
     	    perror("user_handler: write");
     	    close(input_pipe[0]);
     	    close(output_pipe[1]);
     	    exit(1);
             }
     	/* read back from translator */
     	rc = read(input_pipe[0], &ch, 1);
     	c = (int)ch;
     	if (rc <= 0) { /* read failed - notify user and exit. */
     	    perror("user_handler: read");
     	    close(input_pipe[0]);
     	    close(output_pipe[1]);
     	    exit(1);
             }
     	/* print translated character to stdout. */
     	putchar(c);
         }
         /* close pipes and exit. */
         close(input_pipe[0]);
         close(output_pipe[1]);
         exit(0);
     }
     /* now comes the function executed by the translator process. */
     void translator(int input_pipe[], int output_pipe[])
     {
         int c;    /* user input - must be 'int', to recognize EOF (= -1). */
         char ch;  /* the same - as a char. */
         int rc;   /* return values of functions. */
         /* first, close unnecessary file descriptors */
         close(input_pipe[1]); /* we don't need to write to this pipe.  */
         close(output_pipe[0]); /* we don't need to read from this pipe. */
         /* enter a loop of reading from the user_handler's pipe, translating */
         /* the character, and writing back to the user handler.              */
         while (read(input_pipe[0], &ch, 1) > 0) {
     	/* translate any upper-case letter to lower-case. */
             c = (int)ch;
     	if (isascii(c) && isupper(c))
                 c = tolower(c);
             ch = (char)c;
             /* write translated character back to user_handler. */
             rc = write(output_pipe[1], &ch, 1);
             if (rc == -1) { /* write failed - notify user and exit. */
                 perror("translator: write");
                 close(input_pipe[0]);
                 close(output_pipe[1]);
                 exit(1);
             }
         }
         /* close pipes and exit. */
         close(input_pipe[0]);
         close(output_pipe[1]);
         exit(0);
     }
     /* and finally, the main function: spawn off two processes, */
     /* and let each of them execute its function.               */
     int main(int argc, char* argv[])
     {
         /* 2 arrays to contain file descriptors, for two pipes. */
         int user_to_translator[2];
         int translator_to_user[2];
         int pid;       /* pid of child process, or 0, as returned via fork.    */
         int rc;        /* stores return values of various routines.            */
         /* first, create one pipe. */
         rc = pipe(user_to_translator);
         if (rc == -1) {
     	perror("main: pipe user_to_translator");
     	exit(1);
         }
         /* then, create another pipe. */
         rc = pipe(translator_to_user);
         if (rc == -1) {
     	perror("main: pipe translator_to_user");
     	exit(1);
         }
         /* now fork off a child process, and set their handling routines. */
         pid = fork();
         switch (pid) {
     	case -1:	/* fork failed. */
     	    perror("main: fork");
     	    exit(1);
     	case 0:		/* inside child process.  */
     	    translator(user_to_translator, translator_to_user); /* line 'A' */
     	    /* NOT REACHED */
     	default:	/* inside parent process. */
     	    user_handler(translator_to_user, user_to_translator); /* line 'B' */
     	    /* NOT REACHED */
         }
         return 0;	/* NOT REACHED */
     }
  • Обработка символов : isascii() проверяет символ на ASCII-валидность . isupper() проверяет символ на регистр tolower() переводит регистр Код можно найти в архиве в файле two-way-pipe.c.

    Именованные пайпы

    Для анонимных пайпов есть ограничение - с ним может работать только процесс , который его создал . Именованый пайп - или FIFO - реализован с помощью файла . Чтобы прочитать такой пайп , нужно прочитать файл . Чтобы записать в такой пайп , нужно записать в файл . При этом пайп-файл блокируется .

    Создание именованного пайпа с помощью команды mknod

    Это можно сделать с помощью 'mknod' (или 'mkfifo'), или с помощью системного вызова mknod() , или с помощью функции mkfifo() : mknod prog_pipe p Параметр 'p' говорит о его природе.

    Открытие именованного пайпа для чтения или записи

    Открыть пайп можно с помощью системного вызова open() либо функции fopen(). При открытии в первом случае мы получаем файловый дескриптор , во втором - указатель 'FILE' .

    Чтение-запись

    Чтение из именованного пайпа ничем не отличается от чтения из файла , а вот в записи есть отличие :
  • Именованный пайп не может быть открыт одновременно для чтения и записи - нужно выбрать одно из двух .
  • Блокировка именованного пайпа возможна в 2-х случаях - при чтении пустового файла и при попытке записи в файл , открытый на чтение .
  • Несколько слов о сокетах

    Сокеты также могут быть использованы для обмена информацией между процессами . Протоколом может быть TCP, UDP, IP и другие . Существует еще один тип сокета - 'Unix-domain', он чем-то напоминает именованный пайп .

    System V IPC

    Ресурс в IPC может быть private или public. Private - означает , что доступ к ресурсу имеет лишь процесс , который его создал , или порожденный от него процесс. Public - означает , что доступ к ресурсу может иметь любой процесс в системе .

    Режимы доступа и структура 'ipc_perm'

    Ресурсы IPC могут быть защищены аналогично тому , как это делается с файлами/каталогами . При этом ресурс может иметь группу и хозяина . Структура 'ipc_perm': struct ipc_perm { key_t key; /* key identifying the resource */ ushort uid; /* owner effective user ID and effective group ID */ ushort gid; ushort cuid; /* creator effective user ID and effective group ID */ ushort cgid; ushort mode; /* access modes */ ushort seq; /* sequence number */ }; Расшифровка файловых пермишинов :
  • 0400 - владелец прав может читать
  • 0200 - владелец может писать
  • 0040 - группа может читать
  • 0020 - группа может писать
  • 0004 - любой может читать
  • 0002 - любой может писать

    Системные утилиты для администрирования IPC ресурсов

    Нужно постоянно следить за этими ресурсами - удалять устарявшие и т.д. Для этого есть 2 утилиты - 'ipcs' проверяет и 'ipcrm' удаляет.

    Использование Message Queues

    Перед тем , как поговорить о Message Queues , нужно вернуться к пайпам и признаться в том , что программирование пайпов требует нетривиальных усилий : нужно парсить байты , разбивать их на пакеты , предотвращать блокировку чтение/запись и т.д. Message queue - это очередь сообщений . Структура сообщения состоит из 2-х частей - тип (число) и сами данные . Несколько процесов могут писать сообщения в одну очередь или читать из нее .

    Создание Message Queue - msgget()

    Системный вызов msgget() создает очередь . Он имеет 2 параметра - ключ и флаги . Ключи могут быть :
  • IPC_PRIVATE - private очередь
  • a positive integer - public очередь Флаги используются для контроля доступа к очереди . Пример создания private message queue:
     #include <stdio.h>     /* standard I/O routines.            */
     #include <sys/types.h> /* standard system data types.       */
     #include <sys/ipc.h>   /* common system V IPC structures.   */
     #include <sys/msg.h>   /* message-queue specific functions. */
     /* create a private message queue, with access only to the owner. */
     int queue_id = msgget(IPC_PRIVATE, 0600); /* <-- this is an octal number. */
     if (queue_id == -1) {
         perror("msgget");
         exit(1);
     }
  • Системный вызов возвращает целочисленный идентификатор , который будет использоваться для доступа к этой очереди .
  • '0600' пермишин говорит о том , что очередь принадлежит пользователю , который ее создал
  • Структура сообщения - struct msgbuf

    Для сообщения определена системная структура 'msgbuf' :
     struct msgbuf {
         long mtype;     /* message type, a positive number (cannot be zero). */
         char mtext[1];  /* message body array. usually larger than one byte. */
     };
     Пример создания "hello world" message:
     	/* first, define the message string */
     	char* msg_text = "hello world";
     	/* allocate a message with enough space for length of string and */
     	/* one extra byte for the terminating null character.            */
     	struct msgbuf* msg =
     			(struct msgbuf*)malloc(sizeof(struct msgbuf) + strlen(msg_text));
     	/* set the message type. for example - set it to '1'. */
     	msg->mtype = 1;
     	/* finally, place the "hello world" string inside the message. */
     	strcpy(msg->mtext, msg_text);
  • Необходимо помнить о том , что при выделении памяти под строку в юниксе необходимо выделять 1 байт для символа окончания строки . В данном случае этот символ автоматически добавляется самой структурой msgbuf .
  • В сообщение можно положить не только строку , но и бинарные данные , в этом случае от размера бинарных данных нужно вычесть один байт .
  • Запись сообщения в очередь - msgsnd()

    После создания очереди и структуры сообщения , их можно размещать с помощью msgsnd() system call. Ее параметры :
  • int msqid - id очереди
  • struct msgbuf* msg - указатель на структуру
  • int msgsz - размер данных в байтах
  • int msgflg - доп. флаги
  • IPC_NOWAIT - не блокировать процесс при неудаче Пример : int rc = msgsnd(queue_id, msg, strlen(msg_text)+1, 0); if (rc == -1) { perror("msgsnd"); exit(1); }

    Чтение сообщения из очереди - msgrcv()

    Параметры msgrcv():
  • int msqid
  • struct msgbuf* msg
  • int msgsz
  • int msgtyp Пример чтения :
     	/* prepare a message structure large enough to read our "hello world". */
     	struct msgbuf* recv_msg =
     		(struct msgbuf*)malloc(sizeof(struct msgbuf)+strlen("hello world"));
     	/* use msgrcv() to read the message. We agree to get any type, and thus */
     	/* use '0' in the message type parameter, and use no flags (0).         */
     	int rc = msgrcv(queue_id, recv_msg, strlen("hello world")+1, 0, 0);
     	if (rc == -1) {
     		perror("msgrcv");
     		exit(1);
     	}

    Message Queues - пример

    В качестве примера в архиве можно посмотреть : queue_sender queue_reader

    Синхронизация процессов и семафоры

    Одна из основных проблем при написании мульти-процессорного приложения - синхронизация процессов . Для этого можно использовать pipes, sockets , message queues . Другим вариантом являются семафоры .

    Семафоры

    Семафор - это число , которое можно менять что называется на атомарном уровне для синхронизации процессов . Типы операций с семафорами : wait и signal. wait - проверка семафора . Если значение семафора равно проверяемому числу , оно уменьшается на единицу . Если нет , то операция для данного процесса блокируется. signal - увеличивает значение семафора на единицу . semaphore set - структура , хранящая группу семафоров .

    Создание Semaphore Set - semget()

    При этом структуре присваивается id-шник и флаги Пример :
     		/* ID of the semaphore set.     */
     		int sem_set_id_1;
     		int sem_set_id_2;
     		/* create a private semaphore set with one semaphore in it, */
     		/* with access only to the owner.                           */
     		sem_set_id_1 = semget(IPC_PRIVATE, 1, IPC_CREAT | 0600);
     		if (sem_set_id_1 == -1) {
     			perror("main: semget");
     			exit(1);
     		}
     		/* create a semaphore set with ID 250, three semaphores */
     		/* in the set, with access only to the owner.           */
     		sem_set_id_2 = semget(250, 3, IPC_CREAT | 0600);
     		if (sem_set_id_2 == -1) {
     			perror("main: semget");
     			exit(1);
     		}
    Здесь : если структура с ID 250 уже существует , новая не создается .

    semctl()

    В следующем примере мы устанавливаем 3 семафора в 3, 6 и 0. При этом ID 1-го семафора = '0', 2-го - is '1', и т.д. :
     	/* use this to store return values of system calls.   */
     	int rc;
     	/* initialize the first semaphore in our set to '3'.  */
     	rc = semctl(sem_set_id_2, 0, SETVAL, 3);
     	if (rc == -1) {
     		perror("main: semctl");
     		exit(1);
     	}
     	/* initialize the second semaphore in our set to '6'. */
     	rc = semctl(sem_set_id_2, 1, SETVAL, 6);
     	if (rc == -1) {
     		perror("main: semctl");
     		exit(1);
     	}
     	/* initialize the third semaphore in our set to '0'.  */
     	rc = semctl(sem_set_id_2, 2, SETVAL, 0);
     	if (rc == -1) {
     		perror("main: semctl");
     		exit(1);
     	}

    Семафоры и semop()

    Иногда необходимо , чтобы доступ к файлу в текущий момент имел только один процесс . В следующем примере имеется в виду структура с id = "sem_set_id" и семафор со значением = 1 :
     	/* this function updates the contents of the file with the given path name. */
     	void update_file(char* file_path, int number)
     	{
     		/* structure for semaphore operations.   */
     		struct sembuf sem_op;
     		FILE* file;
     		/* wait on the semaphore, unless it's value is non-negative. */
     		sem_op.sem_num = 0;
     		sem_op.sem_op = -1;   /* <-- Comment 1 */
     		sem_op.sem_flg = 0;
     		semop(sem_set_id, &sem_op, 1);
     		/* Comment 2 */
     		/* we "locked" the semaphore, and are assured exclusive access to file.  */
     		/* manipulate the file in some way. for example, write a number into it. */
     		file = fopen(file_path, "w");
     		if (file) {
     			fprintf(file, "%d\n", number);
     			fclose(file);
     		}
     		/* finally, signal the semaphore - increase its value by one. */
     		sem_op.sem_num = 0;
     		sem_op.sem_op = 1;   /* <-- Comment 3 */
     		sem_op.sem_flg = 0;
     		semop(sem_set_id, &sem_op, 1);
     	}
  • Comment 1 - здесь : для доступа к файлу используется semop() , ведущая опрос семафора в режиме wait ; '-1' в sem_op.sem_op означет следующее : если значение семафора больше или равно '1', уменьшаем его на 1. Иначе блокируем процесс до тех пор , пока семафор не станет равным 1
  • Comment 2 - семантика semop() такова , что когда мы из нее возвращаемся , значение семафора = 0 .
  • Comment 3 - после манипуляций с файлом мы увеличиваем семафор до 1
  • Концепция mutex представлена в файле sem-mutex.c, который можно найти в архиве . 5 процессов используют функцию "update_file" . Каждый раз , когда процес изменяет файл , он выводит свой ID . DELAY macro определяет время ожидания между 2-мя вызовами "update_file" , с ним можно поиграть . Семафоры часто используются в модели "producer-consumer" - когда одна группа процессов что-то производит , а другая потребляет . Например , первая группа аккумулирует запросы от клиентов и размещает их в каталоге , а другая группа их читает . Для такой системы необходим счетчик количества таких файлов . Пример :
     	/* this variable will contain the semaphore set. */
     	int sem_set_id;
     	/* semaphore value, for semctl().                */
     	union semun sem_val;
     	/* structure for semaphore operations.           */
     	struct sembuf sem_op;
     	/* first we create a semaphore set with a single semaphore, */
     	/* whose counter is initialized to '0'.                     */
     	sem_set_id = semget(IPC_PRIVATE, 1, 0600);
     	if (sem_set_id == -1) {
     		perror("semget");
     		exit(1);
     	}
     	sem_val.val = 0;
     	semctl(sem_set_id, 0, SETVAL, sem_val);
     	/* we now do some producing function, and then signal the   */
     	/* semaphore, increasing its counter by one.                */
     	.
     	.
     	sem_op.sem_num = 0;
     	sem_op.sem_op = 1;
     	sem_op.sem_flg = 0;
     	semop(sem_set_id, &sem_op, 1);
     	.
     	.
     	.
     	/* meanwhile, in a different process, we try to consume the      */
     	/* resource protected (and counter) by the semaphore.            */
     	/* we block on the semaphore, unless it's value is non-negative. */
     	sem_op.sem_num = 0;
     	sem_op.sem_op = -1;
     	sem_op.sem_flg = 0;
     	semop(sem_set_id, &sem_op, 1);
     
     	/* when we get here, it means that the semaphore's value is '0'  */
     	/* or more, so there's something to consume.                     */
     	.
     	.
    Полный код можно найти в архиве в файле sem-producer-consumer.c.
     
     

    Semaphores - A Complete Example

    Смотрите архив : tiny-lpr.c tiny-lpd.c

    Shared Memory

    As we have seen, many methods were created in order to let processes communicate. All this communications is done in order to share data. The problem is that all these methods are sequential in nature. What can we do in order to allow processes to share data in a random-access manner?
    Shared memory comes to the rescue. As you might know, on a Unix system, each process has its own virtual address space, and the system makes sure no process would access the memory area of another process. This means that if one process corrupts its memory's contents, this does not directly affect any other process in the system.

    With shared memory, we declare a given section in the memory as one that will be used simultaneously by several processes. This means that the data found in this memory section (or memory segment) will be seen by several processes. This also means that several processes might try to alter this memory area at the same time, and thus some method should be used to synchronize their access to this memory area (did anyone say "apply mutual exclusion using a semaphore" ?).


    Background - Virtual Memory Management Under Unix

    In order to understand the concept of shared memory, we should first check how virtual memory is managed on the system.

    In order to achieve virtual memory, the system divides memory into small pages each of the same size. For each process, a table mapping virtual memory pages into physical memory pages is kept. When the process is scheduled for running, its memory table is loaded by the operating system, and each memory access causes a mapping (by the CPU) to a physical memory page. If the virtual memory page is not found in memory, it is looked up in swap space, and loaded from there (this operation is also called 'page in').

    When the process is started, it is being allocated a memory segment to hold the runtime stack, a memory segment to hold the programs code (the code segment), and a memory area for data (the data segment). Each such segment might be composed of many memory pages. When ever the process needs to allocate more memory, new pages are being allocated for it, to enlarge its data segment.

    When a process is being forked off from another process, the memory page table of the parent process is being copied to the child process, but not the pages themselves. If the child process will try to update any of these pages, then this page specifically will be copied, and then only the copy of the child process will be modified. This behavior is very efficient for processes that call fork() and immediately use the exec() system call to replace the program it runs.

    What we see from all of this is that all we need in order to support shared memory, is to some memory pages as shared, and to allow a way to identify them. This way, one process will create a shared memory segment, other processes will attach to them (by placing their physical address in the process's memory pages table). From now all these processes will access the same physical memory when accessing these pages, thus sharing this memory area.


    Allocating A Shared Memory Segment

    A shared memory segment first needs to be allocated (created), using the shmget() system call. This call gets a key for the segment (like the keys used in msgget() and semget()), the desired segment size, and flags to denote access permissions and whether to create this page if it does not exist yet. shmget() returns an identifier that can be later used to access the memory segment. Here is how to use this call:

    
     /* this variable is used to hold the returned segment identifier. */
     int shm_id;
     
     /* allocate a shared memory segment with size of 2048 bytes,      */
     /* accessible only to the current user.                             */
     shm_id = shmget(100, 2048, IPC_CREAT | IPC_EXCL | 0600);
     if (shm_id == -1) {
         perror("shmget: ");
         exit(1);
     }
     

    If several processes try to allocate a segment using the same ID, they will all get an identifier for the same page, unless they defined IPC_EXCL in the flags to shmget(). In that case, the call will succeed only if the page did not exist before.


    Attaching And Detaching A Shared Memory Segment

    After we allocated a memory page, we need to add it to the memory page table of the process. This is done using the shmat() (shared-memory attach) system call. Assuming 'shm_id' contains an identifier returned by a call to shmget(), here is how to do this:

    
     /* these variables are used to specify where the page is attached.  */
     char* shm_addr;
     char* shm_addr_ro;
     
     /* attach the given shared memory segment, at some free position */
     /* that will be allocated by the system.                         */
     shm_addr = shmat(shm_id, NULL, 0);
     if (!shm_addr) { /* operation failed. */
         perror("shmat: ");
         exit(1);
     }
     
     /* attach the same shared memory segment again, this time in     */
     /* read-only mode. Any write operation to this page using this   */
     /* address will cause a segmentation violation (SIGSEGV) signal. */
     shm_addr_ro = shmat(shm_id, NULL, SHM_RDONLY);
     if (!shm_addr_ro) { /* operation failed. */
         perror("shmat: ");
         exit(1);
     }
     

    As you can see, a page may be attached in read-only mode, or in read-write mode. The same page may be attached several times by the same process, and then all the given addresses will refer to the same data. In the example above, we can use 'shm_addr' to access the segment both for reading and for writing, while 'shm_addr_ro' can be used for read-only access to this page. Attaching a segment in read-only mode makes sense if our process is not supposed to alter this memory page, and is recommended in such cases. The reason is that if a bug in our process causes it to corrupt its memory image, it might corrupt the contents of the shared segment, thus causing all other processes using this segment to possibly crush. By using a read-only attachment, we protect the rest of the processes from a bug in our process.


    Placing Data In Shared Memory

    Placing data in a shared memory segment is done by using the pointer returned by the shmat() system call. Any kind of data may be placed in a shared segment, except for pointers. The reason for this is simple: pointers contain virtual addresses. Since the same segment might be attached in a different virtual address in each process, a pointer referring to one memory area in one process might refer to a different memory area in another process. We can try to work around this problem by attaching the shared segment in the same virtual address in all processes (by supplying an address as the second parameter to shmat(), and adding the SHM_RND flag to its third parameter), but this might fail if the given virtual address is already in use by the process.

    Here is an example of placing data in a shared memory segment, and later on reading this data. We assume that 'shm_addr' is a character pointer, containing an address returned by a call to shmat().

    
     /* define a structure to be used in the given shared memory segment. */
     struct country {
         char name[30];
         char capital_city[30];
         char currency[30];
         int population;
     };
     
     /* define a countries array variable. */
     int* countries_num;
     struct country* countries;
     
     /* create a countries index on the shared memory segment. */
     countries_num = (int*) shm_addr;
     *countries_num = 0;
     countries = (struct country*) ((void*)shm_addr+sizeof(int));
     
     strcpy(countries[0].name, "U.S.A");
     strcpy(countries[0].capital_city, "Washington");
     strcpy(countries[0].currency, "U.S. Dollar");
     countries[0].population = 250000000;
     (*countries_num)++;
     
     strcpy(countries[1].name, "Israel");
     strcpy(countries[1].capital_city, "Jerusalem");
     strcpy(countries[1].currency, "New Israeli Shekel");
     countries[1].population = 6000000;
     (*countries_num)++;
     
     strcpy(countries[1].name, "France");
     strcpy(countries[1].capital_city, "Paris");
     strcpy(countries[1].currency, "Frank");
     countries[1].population = 60000000;
     (*countries_num)++;
     
     /* now, print out the countries data. */
     for (i=0; i < (*countries_num); i++) {
         printf("Country %d:\n", i+1);
         printf("  name: %s:\n", countries[i].name);
         printf("  capital city: %s:\n", countries[i].capital_city);
         printf("  currency: %s:\n", countries[i].currency);
         printf("  population: %d:\n", countries[i].population);
     }
     

    A few notes and 'gotchas' about this code:
    1. No usage of malloc().

      Since the memory page was already allocated when we called shmget(), there is no need to use malloc() when placing data in that segment. Instead, we do all memory management ourselves, by simple pointer arithmetic operations. We also need to make sure the shared segment was allocated enough memory to accommodate future growth of our data - there are no means for enlarging the size of the segment once allocated (unlike when using normal memory management - we can always move data to a new memory location using the realloc() function).

    2. Memory alignment.

      In the example above, we assumed that the page's address is aligned properly for an integer to be placed in it. If it was not, any attempt to try to alter the contents of 'countries_num' would trigger a bus error (SIGBUS) signal. further, we assumed the alignment of our structure is the same as that needed for an integer (when we placed the structures array right after the integer variable).

    3. Completeness of the data model.

      By placing all the data relating to our data model in the shared memory segment, we make sure all processes attaching to this segment can use the full data kept in it. A naive mistake would be to place the countries counter in a local variable, while placing the countries array in the shared memory segment. If we did that, other processes trying to access this segment would have no means of knowing how many countries are in there.


    Destroying A Shared Memory Segment

    After we finished using a shared memory segment, we should destroy it. It is safe to destroy it even if it is still in use (i.e. attached by some process). In such a case, the segment will be destroyed only after all processes detach it. Here is how to destroy a segment:

    
     /* this structure is used by the shmctl() system call. */
     struct shmid_ds shm_desc;
     
     /* destroy the shared memory segment. */
     if (shmctl(shm_id, IPC_RMID, &shm_desc) == -1) {
         perror("main: shmctl: ");
     }
     

    Note that any process may destroy the shared memory segment, not only the one that created it, as long as it has write permission to this segment.


    A Complete Example

    As a naive example of using shared memory, we collected the source code from the above sections into a file named shared-mem.c. It shows how a single process uses shared memory. Naturally, when two processes (or more) use a single shared memory segment, there may be race conditions, if one process tries to update this segment, while another is reading from it. To avoid this, we need to use some locking mechanism - SysV semaphores (used as mutexes) come to mind here. An example of two processes that access the same shared memory segment using a semaphore to synchronize their access, is found in the file shared-mem-with-semaphore.c.


    A Generalized SysV Resource ID Creation - ftok()

    One of the problems with SysV IPC methods is the need to choose a unique identifier for our processes. How can we make sure that the identifier of a semaphore in our project won't collide with the identifier of a semaphore in some other program installed on the system?

    To help with that, the ftok() system call was introduced. This system call accepts two parameters, a path to a file and a character, and generates a more-or-less unique identifier. It does that by finding the "i-node" number of the file (more or less the number of the disk sector containing this file's information), combines it with the second parameter, and thus generates an identifier, that can be later fed to semget, shmget() or msgget(). Here is how to use ftok():

    
     /* identifier returned by ftok() */
     key_t set_key;
     
     /* generate a "unique" key for our set, using the */
     /* directory "/usr/local/lib/ourprojectdir".      */
     set_key = ftok("/usr/local/lib/ourprojectdir", 'a');
     if (set_key == -1) {
         perror("ftok: ");
         exit(1);
     }
     
     /* now we can use 'set_key' to generate a set id, for example. */
     sem_set_id = semget(set_key, 1, IPC_CREAT | 0600);
     .
     .
     

    One note should be taken: if we remove the file and then re-create it, the system is very likely to allocate a new disk sector for this file, and thus activating the same ftok call with this file will generate a different key. Thus, the file used should be a steady file, and not one that is likely to be moved to a different disk or erased and re-created.

    Оставьте свой комментарий !

    Ваше имя:
    Комментарий:
    Оба поля являются обязательными

     Автор  Комментарий к данной статье