Сигналы являются программными прерываниями, которые посылаются процессу, когда случается некоторое событие. Сигналы могут возникать синхронно с ошибкой в приложении, например SIGFPE(ошибка вычислений с плавающей запятой) и SIGSEGV(ошибка адресации), но большинство сигналов является асинхронными. Сигналы могут посылаться процессу, когда система обнаруживает программное событие, например, когда пользователь дает команду прервать или остановить выполнение, или сигнал на завершение от другого процесса. Сигналы могут прийти непосредственно от ядра ОС, когда возникает сбой аппаратных средств ЭВМ. Система определяет набор сигналов, которые могут быть отправлены процессу. В Linux существует примерно 30 различных сигналов. При этом каждый сигнал имеет целочисленное значение и приводит к строго определенным действиям.
Механизм передачи сигналов состоит из следующих частей:
Существует три основных варианта реакции на сигналы:
void(*signal(int signr, void(*sighandler)(int)))(int);
Номер | Значение | Реакция программы по умолчанию |
Переменная sighandler определяет функцию обработки сигнала. В заголовочном файле <signal.h> определены две константы SIG_DFL и SIG_IGN. SIG_DFL означает выполнение действий по умолчанию - в большинстве случаев окончание процесса. Например, определение
#include <stdlib.h>
#include <signal.h>
void sigfunc(int sig)
{
char c;
if(sig != SIGINT)
return;
else
{
printf("\nХотите завершить программу (y/n) : ");
while((c=getchar()) != 'n')
return;
exit (0);
}
}
int main()
{
int i;
signal(SIGINT,sigfunc);
while(1)
{
printf(" Вы можете завершить программу с помощью CTRL+C ");
for(i=0;i<=48;i++)
printf("\b");
}
return 0;
}
Пусть приложение работает на отдельном компьютере, и его необходимо преобразовать, чтобы использовать в "распределенной" сети. Ниже показано пошаговое преобразование программы, которая выводит сообщения на терминал.
Однопроцессная версия printmesg.c (рис. 38):
#include <stdio.h>
main(int argc, char *argv[])
{
char *message;
if (argc != 2) {
fprintf(stderr, "usage: %s <message>\n",argv[0]);
exit(1);
}
message = argv[1];
if (!printmessage(message)) {
fprintf(stderr,"%s: невозможно вывести сообщение\n",argv[0]);
exit(1);
}
printf("Сообщение выведено!\n");
exit(0);
}
/* Вывод сообщения на терминал.
* Возвращает логическое значение, показывающее
* выведено ли сообщение. */
printmessage(char *msg)
{
FILE *f;
f = fopen("/dev/console", "w");
if (f == (FILE *)NULL) {
return (0);
}
fprintf(f, "%s\n", msg);
fclose(f);
return(1);
}
Если функцию printmessage() превратить в удаленную процедуру, ее можно вызывать на любой машине сети.
Сначала необходимо определить типы данных всех аргументов вызова процедуры и результата. Аргумент вызова printmessage() - строка, а результат - целое число. Теперь можно написать спецификацию протокола на языке RPC, который будет описывать удаленную версию printmessage(). Исходный код RPC для данной спецификации:
program MESSAGEPROG {
version PRINTMESSAGEVERS {
int PRINTMESSAGE(string) = 1;
} = 1;
} = 0x20000001;
В этом примере, PRINTMESSAGE - это процедура номер 1 в версии 1 удаленной программы MESSAGEPROG с номером программы 0x20000001.
Номера версии увеличиваются, если в удаленной программе изменяются функциональные возможности. При этом могут быть заменены существующие процедуры или добавлены новые. Может быть определена более чем одна версия удаленной программы, а также версия может иметь более одной определенной процедуры.
Необходимо разработать еще две дополнительные программы. Одной из них является сама удаленная процедура. Версия printmsg.c для RPC (рис. 39):
* msg_proc.c: реализация удаленной процедуры "printmessage"
*/
#include <stdio.h>
#include "msg.h" /* msg.h сгенерированный rpcgen */
int * printmessage_1(char **msg, struct svc_req *req)
{
static int result; /* должен быть static! */
FILE *f;
f = fopen("/dev/console", "w");
if (f == (FILE *)NULL) {
result = 0;
return (&result);
}
fprintf(f, "%s\n", *msg);
fclose(f);
result = 1;
return (&result);
}
При этом определение удаленной процедуры printmessage_1 отличается от локальной процедуры printmessage в следующих моментах:
* rprintmsg.c: удаленная версия "printmsg.c"
*/
#include <stdio.h>
#include "msg.h" /* msg.h сгенерирован rpcgen */
main(int argc, char **argv)
{
CLIENT *clnt;
int *result;
char *server;
char *message;
if (argc != 3) {
fprintf(stderr, "usage: %s host
message\n", argv[0]);
exit(1);
}
server = argv[1];
message = argv[2];
/*
* Создает клиентский "обрабочик", используемый
* для вызова MESSAGEPROG на сервере
*/
clnt = clnt_create(server, MESSAGEPROG, PRINTMESSAGEVERS, "visible");
if (clnt == (CLIENT *)NULL) {
/*
* Невозможно установить соединение с сервером.
*/
clnt_pcreateerror(server);
exit(1);
}
/*
* Вызов удаленной процедуры
* "printmessage" на сервере
*/
result = printmessage_1(&message, clnt);
if (result == (int *)NULL) {
/*
* Ошибка при вызове сервера
*/
clnt_perror(clnt, server);
exit(1);
}
/* Успешный вызов удаленной процедуры.
*/
if (*result == 0) {
/*
* Сервер не может вывести сообщение.
*/
fprintf(stderr, "%s: невозможно вывести сообщение\n",argv[0]);
exit(1);
}
/* Сообщение выведено на терминал сервера
*/
printf("Сообщение доставлено %s\n", server);
clnt_destroy( clnt );
exit(0);
}
Следует отметить следующие особенности клиентской программы вызова printmsg.c:
Для компиляции примера удаленного rprintmsg:
Откомпилируйте протокол, определенный в msg.x: rpcgen msg.x. При этом будут созданы заголовочный файл (msg.h), клиентская часть (msg_clnt.c), и серверная часть (msg_svc.c).
Откомпилируйте исполняемый файл клиента:
В этом примере не были созданы никакие процедуры XDR, потому что приложение использует только основные типы, которые включены в libnsl. Теперь нужно рассмотреть, что создает rpcgen на основе входного файла msg.x:
rpcgen можно использовать для создания процедур XDR, которые будут преобразовывать локальные структуры данных в формат XDR и наоборот.
Пусть dir.x содержит сервис удаленного чтения каталога, созданный с помощью rpcgen, и содержащий процедуры сервера и процедуры XDR.
Файл описания протокола RPC dir.x имеет следующий вид:
* dir.x: Протокол вывода удаленного каталога
*/
const MAXNAMELEN = 255; /*максимальная длина элемента каталога */
typedef string nametype<MAXNAMELEN>; /* элемент каталога */
typedef struct namenode *namelist; /* ссылка в списке */
/* Узел в списке каталога */
struct namenode {
nametype name; /* имя элемента каталога */
namelist next; /* следующий элемент */
};
union readdir_res switch (int errno) {
case 0:
namelist list; /* нет ошибок: возвращает оглавление
каталога */
default:
void; /* возникла ошибка: возвращать нечего */
};
/* Определение программы каталога */
program DIRPROG {
version DIRVERS {
readdir_res
READDIR(nametype) = 1;
} = 1;
} = 0x20000076;
При запуске rpcgen для dir.x создаются четыре файла:
Серверная часть процедуры READDIR, в файле dir_proc.c показана ниже (рис. 41):
* dir_proc.c: удаленная реализация readdir
*/
#include <dirent.h>
#include "dir.h" /* Создается rpcgen */
extern int errno;
extern char *malloc();
extern char *strdup();
readdir_res *
readdir_1(nametype *dirname, struct svc_req *req)
{
DIR *dirp;
struct dirent *d;
namelist nl;
namelist *nlp;
static readdir_res res; /* должен быть static! */
/* Открыть каталог */
dirp = opendir(*dirname);
if (dirp == (DIR *)NULL) {
res.errno = errno;
return (&res);
}
/* Очистить предыдущий результат */
xdr_free(xdr_readdir_res, &res);
/*
* Собрать элементы каталога.
*/
nlp = &res.readdir_res_u.list;
while (d = readdir(dirp)) {
nl = *nlp = (namenode *)
malloc(sizeof(namenode));
if (nl == (namenode *) NULL) {
res.errno = EAGAIN;
closedir(dirp);
return(&res);
}
nl->name = strdup(d->d_name);
nlp = &nl->next;
}
*nlp = (namelist)NULL;
/* Вывести результат */
res.errno = 0;
closedir(dirp);
return (&res);
}
Клиентская часть процедуры READDIR, файл rls.c приведена ниже (рис. 42):
* rls.c: Клиент для удаленного чтения каталогов
*/
#include <stdio.h>
#include "dir.h" /* создается rpcgen */
extern int errno;
main(int argc, char *argv[])
{
CLIENT *clnt;
char *server;
char *dir;
readdir_res *result;
namelist nl;
if (argc != 3) {
fprintf(stderr, "usage: %s host
directory\n",argv[0]);
exit(1);
}
server = argv[1];
dir = argv[2];
/*
* Создает обработчик клиента,
* вызывающий MESSAGEPROG на сервере
*/
cl = clnt_create(server, DIRPROG, DIRVERS, "tcp");
if (clnt == (CLIENT *)NULL) {
clnt_pcreateerror(server);
exit(1);
}
result = readdir_1(&dir, clnt);
if (result == (readdir_res *)NULL) {
clnt_perror(clnt, server);
exit(1);
}
/* Успешный вызов удаленной процедуры. */
if (result->errno != 0) {
/* Ошибка на удаленной системе.
*/
errno = result->errno;
perror(dir);
exit(1);
}
/* Оглавление каталога получено.
* Вывод на экран.
*/
for (nl = result->readdir_res_u.list;
nl != NULL;
nl = nl->next) {
printf("%s\n", nl->name);
}
xdr_free(xdr_readdir_res, result);
clnt_destroy(cl);
exit(0);
}
Код клиента, создаваемый rpcgen, не освобождает память, выделенную для результатов запроса RPC. Поэтому следует вызывать xdr_free(), чтобы освободить память после завершения работы. Это похоже на вызов free(), за исключением того, что здесь для получения результата передается процедура XDR.
rpcgen поддерживает препроцессор C. При этом препроцессор C применяется к входным файлам rpcgen перед компиляцией. В исходных файлах .x поддерживаются все стандартные директивы препроцессора C. В зависимости от типа генерируемого выходного файла, пять символов определяются самим rpcgen. rpcgen обеспечивает поддержку дополнительных возможностей препроцессинга: любая строка, которая начинается с символа процента (%), передается непосредственно в выходной файл, независимо от содержания.
Следующие символы можно использовать, чтобы создать файл определенного вида:
* time.x: Удаленный протокол времени
*/
program TIMEPROG {
version TIMEVERS {
unsigned int TIMEGET() = 1;
} = 1;
} = 0x20000044;
#ifdef RPC_SVC
%int *
%timeget_1()
%{
% static int thetime;
%
% thetime = time(0);
% return (&thetime);
%}
#endif
This document was generated using the LaTeX2HTML translator Version 2002-2-1 (1.70)
Copyright © 1993, 1994, 1995, 1996,
Nikos Drakos,
Computer Based Learning Unit, University of Leeds.
Copyright © 1997, 1998, 1999,
Ross Moore,
Mathematics Department, Macquarie University, Sydney.
The command line arguments were:
latex2html mainfile.tex
The translation was initiated by on 2003-12-09
Функция raise() имеет следующий вид:
int raise(int sig);
int main()
{
int a,b;
printf("Число : ");
scanf("%d",&a);
printf("делится на : ");
scanf("%d",&b);
if(b==0)
raise(SIGFPE);
else
printf("Результат = %d\n",a/b);
return 0;
}
Для передачи сигнала некоторому процессу существует функция kill(), синтаксис которой приведен ниже.
#include <signal.h>
int kill(pid_t pid, int signalnumber);
В системах Linux концепция сигналов была расширена. Это вызвано следующими недостатками старого ANSI-C подхода:
int sigemptyset(sigset_t *sig_m);
int sigaddset(sigset_t *sig_m, int signr);
int sigdelset(setsig_t *sig_m, int signr);
int sigismember(sigset_t sig_m,int signr);
Существует функция для сохранения или изменения маски сигналов:
int sigprocmask(int mode, const sigset_t *sig_m, sigset_t *alt_sig_m);
int sigsuspend(const sigset_t *sig_m);
Сигналы позволяют осуществить самый примитивный способ коммуникации между двумя процессами. С помощью функции kill() процесс может послать сигнал другому процессу. Затем процесс может реагировать на принятый сигнал. Разумеется, в качестве IPC сигналы используются крайне редко. В качестве примера приведена программа, которая создает с помощью fork() второй процесс. Затем оба процесса (родитель и потомок) обмениваются данными и выводят сообщения на экран. При этом потомок переводится в состояние ожидания, пока родительский процесс выводит сообщение. Родитель посылает сигнал потомку посредством kill(), а затем сам переводится в состояние ожидания. Потомок выводит сообщение, будит родительский процесс и переводится в состояние ожидания и т.д. Программа приведена на рис. 8.
#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
enum { FALSE, TRUE };
sigset_t sig_m1, sig_m2, sig_null;
int signal_flag=FALSE;
void sig_func(int signr)
{
start_signalset();
signal_flag = TRUE;
}
void start_signalset()
{
if(signal(SIGUSR1, sig_func) == SIG_ERR)
exit(0);
if(signal(SIGUSR2, sig_func) == SIG_ERR)
exit(0);
sigemptyset(&sig_m1);
sigemptyset(&sig_null);
sigaddset(&sig_m1,SIGUSR1);
sigaddset(&sig_m1,SIGUSR2);
if(sigprocmask(SIG_BLOCK, &sig_m1, &sig_m2) < 0)
exit(0);
}
void message_for_parents(pid_t pid)
{
kill(pid,SIGUSR2);
}
void wait_for_parents()
{
while(signal_flag == FALSE)
sigsuspend(&sig_null);
signal_flag = FALSE;
if(sigprocmask(SIG_SETMASK, &sig_m2, NULL) < 0)
exit(0);
}
void message_for_child(pid_t pid)
{
kill(pid, SIGUSR1);
}
void wait_for_child(void)
{
while(signal_flag == FALSE)
sigsuspend(&sig_null);
signal_flag = FALSE;
if(sigprocmask(SIG_SETMASK, &sig_m2, NULL) < 0)
exit(0);
}
int main()
{
pid_t pid;
char x,y;
start_signalset();
switch( pid = fork())
{
case -1 : fprintf(stderr, "Ошибка fork()\n");
exit(0);
case 0 : /*...в потомке...*/
for(x=2;x<=10;x+=2)
{
wait_for_parents();
write(STDOUT_FILENO, "ping-",strlen("ping-"));
message_for_parents(getppid());
}
exit(0);
default : /*...в родителе....*/
for(y=1;y<=9;y+=2)
{
write(STDOUT_FILENO, "pong-", strlen("pong-"));
message_for_child(pid);
wait_for_child();
}
}
printf("\n\n");
return 0;
}
Труба является однонаправленным коммуникационным каналом между двумя процессами. Кроме поддержки коммуникации труба может использоваться для контроля информационного потока между двумя процессами. Это происходит потому, что труба может принимать только определенный объем данных (обычно 4 Кб). Если труба заполнена, процесс останавливается до тех пор, пока хотя бы один байт не будет прочитан из этой трубы и не появится свободное место, чтобы снова заполнять ее данными. С другой стороны, если труба пуста, то читающий процесс останавливается до тех пор, пока пишущий процесс не пошлет что-либо в эту трубу.
Труба владеет двумя дескрипторами файлов. Первый дескриптор служит для чтения, а второй для записи в трубу:
int pipe(int fd[2]);
Второй процесс для обмена можно создать с помощью fork(). Процесс-потомок наследует от родителя оба открытых дескриптора файлов. Необходимо указать обоим процессам, кто куда пишет и кто что читает, при этом закрыв ненужные дескрипторы.
В приведенном ниже примере (рис. 9) процесс-родитель будет записывать данные в трубу. Таким образом, дескриптор чтения (fd[0]) родительского процесса закрывается. Дескриптор записи потомка так же закрывается. Потомок будет только читать данные из трубы.
#include <sys/wait.h>
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#define USAGE printf("usage : %s данные\n",argv[0]);
#define MAX 4096
int main(int argc, char *argv[])
{
int fd[2], fd1,i, n;
pid_t pid;
char buffer[MAX];
FILE *dataptr;
if(argc !=2)
{ USAGE; exit(0); }
if((fd1=open(argv[1], O_RDONLY)) < 0)
{ perror("open : "); exit(0); }
/*Устанавливаем трубу*/
if(pipe(fd) < 0)
{ perror("pipe : "); exit(0); }
/*Создаем новый процесс*/
if((pid=fork()) < 0)
{ perror("pipe : "); exit(0); }
else if(pid > 0) /*Это родитель*/
{
close(fd[0]); /*Закрываем чтение*/
n=read(fd1, buffer, MAX);
if((write(fd[1], buffer, n)) != n)
{ perror(" write : "); exit(0); }
if((waitpid(pid, NULL, 0)) < 0)
{ perror("waitpid : "); exit(0); }
}
else /*Это потомок*/
{
close(fd[1]); /*Закрываем запись*/
n=read(fd[0], buffer, MAX);
if((write(STDOUT_FILENO, buffer, n)) != n)
{ perror(" write : "); exit(0); }
}
exit(0);
}
Чтобы избавить программиста от излишнего кода, для работы с трубами введена функция popen(). Синтаксис этой функции:
#include <sys/wait.h>
#include <stdio.h>
#include <sys/types.h>
#include <fcntl.h>
#define EXIT(s) {fprintf(stderr, "%s",s); exit(0);}
#define USAGE(s) {fprintf(stderr, "%s Данные для чтения\n",s); exit(0);}
#define MAX 8192
enum {ERROR=-1,SUCCESS};
int main(int argc, char **argv)
{
FILE *pipe_writer, *file;
char buffer[MAX];
if(argc!=2)
USAGE(argv[0]);
if((file=fopen(argv[1], "r")) == NULL)
EXIT("Ошибка открытия файла.........\n");
if(( pipe_writer=popen("./filter" ,"w")) == NULL)
EXIT("Ошибка открытия трубы...........\n");
while(1)
{
if(fgets(buffer, MAX, file) == NULL)
break;
if(fputs(buffer, pipe_writer) == EOF)
EXIT("Ошибка записи........\n");
}
pclose(pipe_writer);
}
С помощью труб могут общаться только родственные друг другу процессы, полученные с помощью fork(). Именованные каналы FIFO дают возможность обмена данными с абсолютно чужим процессом.
С точки зрения ядра ОС FIFO является одним из вариантов реализации трубы. Системный вызов mkfifo() предоставляет именованную трубу в виде объекта файловой системы. Как и для любого другого объекта, необходимо предоставлять процессам права доступа в FIFO, чтобы определить, кто может писать что-либо в FIFO, и кто может читать из нее. Несколько процессов могут записывать или читать FIFO одновременно. Режим работы с FIFO - полудуплексный, т.е. процессы могут общаться в одном направлении. Типичное применение FIFO - разработка приложений клиент-сервер.
Синтаксис функции для создания FIFO следующий:
#include <sys/stat.h>
int mkfifo(const char *fifoname, mode_t mode);
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
int fd_fifo; /*дескриптор FIFO*/
char buffer[]="Текстовая строка для fifo\n";
char buf[100];
/*Если файл с таким именем существует, удалим его*/
unlink("/tmp/fifo0001.1");
/*Создаем FIFO*/
if((mkfifo("/tmp/fifo0001.1", O_RDWR)) == -1)
{
fprintf(stderr, "Невозможно создать fifo.........\n");
exit(0);
}
/*Открываем fifo для чтения и записи*/
if((fd_fifo=open("/tmp/fifo0001.1", O_RDWR)) == - 1)
{
fprintf(stderr, "Невозможно открыть fifo.....\n");
exit(0);
}
write(fd_fifo,buffer,strlen(buffer)) ;
if(read(fd_fifo, &buf, sizeof(buf)) == -1)
fprintf(stderr, "Невозможно прочесть из FIFO.......\n");
else
printf("Прочитано из FIFO : %s\n",buf);
return 0;
}
Если в системе отсутствует функция mkfifo(), можно воспользоваться общей функцией для создания файла
{ /*Невозможно создать fifo */
Флаг O_NONBLOCK может использоваться только при доступе для чтения. При попытке открыть FIFO с O_NONBLOCK для записи возникает ошибка открытия. Если FIFO закрыть для записи через close или fclose, это значит, что для чтения в FIFO помещается EOF.
Если несколько процессов пишут в один и тот же FIFO, необходимо обратить внимание на то, чтобы сразу не записывалось больше чем PIPE_BUF байтов. Это необходимо, чтобы данные не смешивались друг с другом. Установить пределы записи можно следующей программой (рис. 12):
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main()
{
unlink("fifo0001");
/*Создаем новый FIFO*/
if((mkfifo("fifo0001", O_RDWR)) == -1)
{
fprintf(stderr, "Невозможно создать FIFO\n");
exit(0);
}
printf("Можно записать в FIFO сразу %ld байтов\n",
pathconf("fifo0001", _PC_PIPE_BUF));
printf("Одновременно можно открыть %ld FIFO \n", sysconf(_SC_OPEN_MAX));
return 0;
}
При попытке записи в FIFO, который не открыт в данный момент для чтения ни одним процессом, генерируется сигнал SIGPIPE.
В следующем примере организуется обработчик сигнала SIGPIPE, создается FIFO, процесс-потомок записывает данные в этот FIFO, а родитель читает их оттуда. Пример иллюстрирует простое приложение типа клиент/сервер (рис. 13).
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <signal.h>
static volatile sig_atomic_t sflag;
static sigset_t signal_new, signal_old, signal_leer;
static void sigfunc(int sig_nr)
{
fprintf(stderr, "SIGPIPE вызывает завершение программы\n");
exit(0);
}
void signal_pipe(void)
{
if(signal(SIGPIPE, sigfunc) == SIG_ERR)
{
fprintf(stderr, "Невозможно получить сигнал SIGPIPE\n");
exit(0);
}
/*Удаляем все сигналы из множества сигналов*/
sigemptyset(&signal_leer);
sigemptyset(&signal_new);
sigaddset(&signal_new, SIGPIPE);
/*Устанавливаем signal_new и сохраняем его*/
/* теперь маской сигналов будет signal_old*/
if(sigprocmask(SIG_UNBLOCK, &signal_new, &signal_old) < 0)
exit(0);
}
int main()
{
int r_fifo, w_fifo; /*дескрипторы FIFO*/
char buffer[]="Текстовая строка для fifo\n";
char buf[100];
pid_t pid;
signal_pipe();
unlink("/tmp/fifo0001.1");
/*Создаем FIFO*/
if((mkfifo("/tmp/fifo0001.1", O_RDWR)) == -1)
{
fprintf(stderr, "Невозможно создать fifo.........\n");
exit(0);
}
pid=fork();
if(pid == -1)
{ perror("fork"); exit(0);}
else if(pid > 0) /*Родитель читает из FIFO*/
{
if (( r_fifo=open("/tmp/fifo0001.1", O_RDONLY)) < 0)
{ perror("r_fifo open"); exit(0); }
while(wait(NULL)!=pid); /*Ждем окончания потомка*/
read(r_fifo, &buf, sizeof(buf)); /*Читаем из FIFO*/
printf("%s\n",buf);
close(r_fifo);
}
else /*Потомок записывает в FIFO*/
{
if((w_fifo=open("/tmp/fifo0001.1", O_WRONLY)) < 0)
{ perror("w_fifo open"); exit(0); }
write(w_fifo, buffer, strlen(buffer)); /*Записываем в FIFO*/
close(w_fifo); /*EOF*/
exit(0);
}
return 0;
}
Одной из проблем при организации параллельного доступа к файлам из нескольких процессов является непредсказуемость порядка доступа в файл. В следующем примере два процесса записывают в файл строку символов по одному символу (рис. 14):
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>
int main()
{
int fd;
int x,y;
pid_t pid;
unlink("/tmp/file"); /*Удаляем предыдущий файл*/
fd=open("/tmp/file", O_WRONLY|O_CREAT, 0777);
if(fd==-1)
{
perror("open : ");
exit(0);
}
if((pid=fork()) == -1)
{
perror("fork :");
exit(0);
}
else if(pid)
{ /*Родительский процесс*/
while(1)
{
for(x=0; x<10; x++){
sleep(1);
write(fd,(char *)"x",1);
}
break;
}
else
{ /*Процесс-потомок*/
while(1)
{
for(y=0; y<10; y++){
sleep(1);
write(fd,(char *)"X",1);
}
break;
}
return 0;
}
После отработки этой программы содержимое файла ``/tmp/file'' может быть следующим:
Общая тенденция развития современных информационных технологий состоит в активном использовании открытых и распределенных систем, а также новых архитектур приложений, таких, как клиент/сервер и параллельные процессы.
Чтобы различные программы могли обмениваться информацией через файлы или общую базу данных, они, как правило, работают через соответствующий API (Application Programming Interface - интерфейс программирования приложения). Если программы находятся на различных компьютерах, то процесс взаимодействия сопряжен с определенными дополнительными трудностями, например, ограниченной пропускной способностью и сложностью синхронизации. Для организации коммуникации между одновременно работающими процессами применяются средства IPC (Interprocess Communication).
Выделяются три уровня IPC: локальные, удаленные и высокоуровневые.
Локальные IPC привязаны к процессору и возможны только в пределах компьютера. К этому виду IPC принадлежат практически все механизмы IPC UNIX, а именно трубы, разделяемая память и очереди сообщений. Коммуникации, или адресное пространство IPC, поддерживаются только в пределах компьютерной системы. Из-за этих ограничений для них могут реализовываться более простые и более быстрые интерфейсы.
Удаленные IPC представляют механизмы, которые обеспечивают взаимодействие как в пределах одного процессора, так и между программами на различных процессорах, соединенных через сеть. Сюда относятся удаленные вызовы процедур (Remote Procedure Calls - RPC), сокеты Unix, а также TLI (Transport Layer Interface - интерфейс транспортного уровня) фирмы Sun.
Под высокоуровневыми IPC обычно подразумеваются пакеты программного обеспечения, которые обеспечивают промежуточный слой между системной платформой и приложением. Эти пакеты предназначены для переноса уже испытанных протоколов коммуникации приложения на более новую архитектуру.
Средства IPC, впервые реализованные в UNIX 4.2BSD, включали в себя многие современные идеи, одновременно пытаясь соответствовать философии UNIX с точки зрения простоты и краткости.
До введения механизмов межпроцессного взаимодействия UNIX не обладал удобными возможностями для обеспечения подобных услуг. Единственным стандартным механизмом, который позволял двум процессам связываться между собой, были трубы (pipes). К сожалению, трубы имели очень серьезное ограничение в том, что два поддерживающих связь процесса должны были быть связаны через общего предка. Кроме того, семантика труб почти не позволяет поддержку распределенной вычислительной среды.
Основные проблемы при введении новых средств IPC в состав системы были связаны с тем фактом, что старые средства были привязаны к файловой системе UNIX либо через обозначение, либо через реализацию. Поэтому новые средства IPC были разработаны как полностью независимая подсистема. Вследствие этого, они позволяют процессам взаимодействовать различными способами. Процессы могут взаимодействовать либо через пространство имен, подобное файловой системе UNIX, либо через сетевое пространство имен. В процессе работы новые пространства имен могут быть добавлены с незначительными изменениями, заметными пользователю. Кроме того, средства взаимодействия были расширены, чтобы поддерживать другие форматы передачи, кроме простого байтового потока, обеспечиваемого трубой. Эти расширения вылились в полностью новую часть системы, которая требует детального знакомства.
Простые межпроцессные коммуникации можно организовать с помощью сигналов и труб. Кроме этого, существуют еще более сложные средства IPC, например, очереди сообщений, семафоры и разделяемые области памяти.
Наряду с обеспечением взаимодействия процессов, средства IPC призваны решать проблемы, возникающие при организации параллельных вычислений. Сюда относятся:
Эта структура имеет следующее содержание:
short l_type; /*3 режима блокирования F_RDLCK(Разделение чтения)
F_WRLCK (Разделение записи)
F_UNLCK (Прекратить разделение)*/
off_t l_start; /*относительное смещение в байтах,
зависит от l_whence*/
short l_whence; /*SEEK_SET;SEEK_CUR;SEEK_END*/
off_t l_len; /*длина, 0=разделение до конца файла*/
pid_t l_pid; /*идентификатор, возвращается F_GETLK */
};
flockptr.l_whence=SEEK_SET; /*с начала файла*/
Функции блокирования можно осуществлять в следующих режимах.
С помощью этого режима можно установить, какой вид блокировки применяется к данному файлу:
fd=open(data, O_CREAT|O_WRONLY);
fcntl(fd, F_GETLK, &flptr);
if(flptr.l_type==F_UNLCK)
{/*Данные не блокированы*/}
else if(flptr.l_type==F_RDLCK)
{/*Блокировка по чтению*/}
else if(flptr.l_type==F_WRLCK)
{/*Блокировка по записи*/}
С помощью этой опции организуется блокировка. Если блокирование невозможно, функция fcntl заканчивается и устанавливает переменную errno в заначение EACCES или EAGAIN. С помощью F_SETLK можно также удалить установленную блокировку (F_UNLCK).
flptr.l_whence=SEEK_SET;
flptr.l_len=0;
flptr.l_type=F_RDLCK;
if((fcntl(fd, F_SETLK, &flptr)!=-1)
{/*Установлена блокировка по чтению*/}
flptr.l_start=0;
flptr.l_whence=SEEK_SET;
flptr.l_len=0;
flptr.l_type=F_WRLCK);
if((fcntl(fd, F_SETLK, &sperre)!=-1)
{/*Установлена блокировка по записи*/}
flptr.l_type=F_UNLCK;
if((fcntl(fd, F_SETLK, &flptr)!=-1)
{/*Блокировка снята*/}
С помощью этой команды процесс приостанавливается до тех пор, пока он не сможет организовать необходимую блокировку.
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#define FNAME "locki.lck"
void status(struct flock *lock)
{
printf("Status: ");
switch(lock->l_type)
{
case F_UNLCK: printf("F_UNLCK (Блокировка снята)\n");
break;
case F_RDLCK: printf("F_RDLCK (pid: %d) (Блокировка по чтению)\n",
lock->l_pid);
break;
case F_WRLCK: printf("F_WRLCK (pid: %d) (Блокировка по записи)\n",
lock->l_pid);
break;
default : break;
}
}
int main(int argc, char **argv)
{
struct flock lock;
int fd;
char buffer[100];
fd = open(FNAME, O_WRONLY|O_CREAT|O_APPEND, S_IRWXU);
memset(&lock, 0, sizeof(struct flock));
/*Проверим, установлена ли блокировка*/
fcntl(fd, F_GETLK, &lock);
if(lock.l_type==F_WRLCK || lock.l_type==F_RDLCK)
{
status(&lock);
memset(&lock,0,sizeof(struct flock));
lock.l_type = F_UNLCK;
if (fcntl(fd, F_SETLK, &lock) < 0)
printf("Fehler : fcntl(fd, F_SETLK, F_UNLCK) (%s)\n",
strerror(errno));
else
printf("Успешно снята блокировка:
fcntl(fd, F_SETLK, F_UNLCK)\n");
}
status(&lock);
write(STDOUT_FILENO,"\nВВедены данные: ",
sizeof("\nВведены данные: "));
while((read(1,puffer,100))>1)
write(fd,buffer,100);
memset(&lock, 0, sizeof(struct flock));
lock.l_type = F_WRLCK;
lock.l_start=0;
lock.l_whence = SEEK_SET;
lock.l_len=0;
lock.l_pid = getpid();
if (fcntl(fd, F_SETLK, &lock) < 0)
printf("Ошибка при: fcntl(fd, F_SETLK, F_WRLCK)(%s)\n",
strerror(errno));
else
printf("Успешно: fcntl(fd, F_SETLK, F_WRLCK)\n");
status(&lock);
switch(fork())
{
case -1 : exit(0);
case 0 : if(lock.l_type == F_WRLCK)
{
printf("Потомок:Невозможна запись
в файл (F_WRLCK)\n");
exit(0);
}
else
printf("Потомок готов к записи в файл\n") ;
exit(0);
default : wait(NULL); break;
}
close(fd);
return 0;
}
Этот пример показывает возможности сериализации процессов. Тем не менее, здесь все равно существует возможность чтения и записи в процессе-потомке. Такой вариант блокировки называется advisory locking (необязательная блокировка). При его установке не производится никакая дополнительная проверка, должны ли системные функции open, read и write при вызове запрещаться из-за блокировки. Для advisory locking принимается, что пользователь сам является ответственным за проверку того, существуют ли определенные блокировки или нет. Невозможно запретить ВСЕМ процессам доступ к файлу. Только процессы, опрашивающие наличие блокировки с помощью fcntl, блокируются при ее наличии (не всегда).
Для других случаев имеются так называемые строгие блокировки (mandatory locking). Строгие блокировки запрещают процессу получать доступ функциями read или write к данным, которые были блокированы другим процессом ранее через fcntl.
Строгие блокировки можно разрешить, установив бит Set-Group-ID и сняв бит выполнения для группы, например:
{
struct stat statbuffer;
if(fstat(fd, &statbuffer) < 0)
{
fprintf(stderr, "Ошибка при вызове fstat.......\n");
return 0;
}
if(fchmod(fd (statbuffer.st_mode & ~S_IXGRP) | S_ISGID) < 0)
{
fprintf(stderr, "Невозможно установить строгую блокировку...\n");
return 0;
}
return 1;
}
Если с помощью open() открывается файл с флагами O_TRUNC и O_CREAT и для этого файла установлена строгая блокировка, то возвращается ошибка со значением errno=EAGAIN.
Взаимная блокировка процессов может возникнуть из-за блокировки файлов. Пусть, например, процесс номер 1 пытается установить блокировку в некотором файле dead.txt в позиции 10.
Другой процесс с номером 2 организует блокировку того же самого файла в позиции 20. Эта ситуация еще управляема. Далее процесс 1 хочет организовать следующую блокировку в позиции 20, где уже стоит блокировка процесса 2. При этом используется команда F_SETLKW. При этом процесс 1 приостанавливается до тех пор, пока процесс 2 снова не освободит со своей стороны блокировку в позиции 20. Теперь процесс 2 пытается организовать в позиции 10, где процесс 1 уже поставил блокировку, такую же блокировку командой F_SETLKW, и также приостанавливается и ждет, пока процесс 1 снимет блокировку. Теперь оба процесса, номер 1 и номер 2, приостановлены и оба ждут друг друга (F_SETLKW), образуя дедлок. Никакой из процессов не возобновит свое выполнение.
Причины, по которым эта ситуация возникает, во многом вызваны неудачным проектированием алгоритмов. В UNIX не предусмотрены механизмы определения и предотвращения тупика. Наличие этих механизмов существенно влияет на производительность системы, поэтому ответственность за предотвращение тупика ложится на программиста.
Пример программы, вызывающей тупик, приведен ниже (рис. 16):
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
extern int errno;
void status(struct flock *lock)
{
printf("Status: ");
switch(lock->l_type)
{
case F_UNLCK: printf("F_UNLCK\n"); break;
case F_RDLCK: printf("F_RDLCK (pid: %d)\n", lock->l_pid); break;
case F_WRLCK: printf("F_WRLCK (pid: %d)\n", lock->l_pid); break;
default : break;
}
}
void writelock(char *proсess, int fd, off_t from, off_t to)
{
struct flock lock;
lock.l_type = F_WRLCK;
lock.l_start=from;
lock.l_whence = SEEK_SET;
lock.l_len=to;
lock.l_pid = getpid();
if (fcntl(fd, F_SETLKW, &lock) < 0)
{
printf("%s : fcntl(fd, F_SETLKW, F_WRLCK) failed (%s)\n",
proсess,strerror(errno));
printf("\nВозник DEADLOCK (%s - proсess)!!!!!!!!!\n\n",proсess);
exit(0);
}
else
printf("%s : fcntl(fd, F_SETLKW, F_WRLCK) успешно\n",proсess);
status(&lock);
}
int main()
{
int fd, i;
pid_t pid;
if(( fd=creat("dead.txt", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH))<0)
{
fprintf(stderr, "Ошибка при создании......\n");
exit(0);
}
/*Заполняем dead.txt 50 байтами символа X*/
for(i=0; i<50; i++)
write(fd, "X", 1);
if((pid = fork()) < 0)
{
fprintf(stderr, "Ошибка fork()......\n");
exit(0);
}
else if(pid == 0) //Потомок
{
writelock("Потомок", fd, 20, 0);
sleep(3);
writelock("Потомок" , fd, 0, 20);
}
else //Родитель
{
writelock("Родитель", fd, 0, 20);
sleep(1);
writelock ("Родитель", fd, 20, 0);
}
exit(0);
}
Вначале создается файл данных dead.txt, в который записывается 50 символов X. Затем родительский процесс организует блокировку от байта 0 до байта 19, а потомок - блокировку от байта 20 до конца файла (EOF). Потомок засыпает на 3 сек., а родитель теперь устанавливает блокировку от байта 20 до байта EOF и при приостанавливается, так как байты от 20 до EOF блокированы в данный момент потомком, и используется команда F_SETLKW. Наконец, потомок пытается установить блокировку на запись от байта 0 до байта 19, причем он также приостанавливается, так как в этой области уже существует блокировка родителя и используется команда F_SETLKW. Здесь возникает тупик, что подтверждается выдачей кода ошибки для errno = EDEADLK (возникновение тупика по ресурсам). Тупик может возникнуть только при использовании команды F_SETLKW. Если применять команду F_SETLK, код ошибки для errno = EAGAIN (Ресурс временно недоступен).
Очереди сообщений как средство межпроцессной связи дают возможность процессам взаимодействовать, обмениваясь данными. Данные передаются между процессами дискретными порциями, называемыми сообщениями. Процессы, использующие этот тип межпроцессной связи, могут выполнять две операции:
Процессы, имеющие права на операции и пытающиеся послать или принять сообщение, могут приостанавливаться, если выполнение операции не было успешным. В частности это означает, что процесс, пытающийся послать сообщение, может ожидать, пока процесс-получатель не будет готов; наоборот, получатель может ждать отправителя. Если указано, что процесс в таких ситуациях должен приостанавливаться, говорят о выполнении над сообщением ``операции с блокировкой''. Если приостанавливать процесс нельзя, говорят, что над сообщением выполняется ''операция без блокировки''.
Процесс, выполняющий операцию с блокировкой, может быть приостановлен до тех пор, пока не будет удовлетворено одно из условий:
Перед тем, как посылать или принимать сообщения, должны быть созданы очередь сообщений с уникальным идентификатором и ассоциированная с ней структура данных. Порожденный уникальный идентификатор называется идентификатором очереди сообщений (msqid); он используется для обращений к очереди сообщений и ассоциированной структуре данных.
Говоря об очереди сообщений следует иметь в виду, что реально в ней хранятся не сами сообщения, а их описатели, имеющие следующую структуру:
struct msg *msg_next; /* Указатель на следующее сообщение */
long msg_type; /* Тип сообщения */
short msg_ts; /* Размер текста сообщения */
short msg_spot; /* Адрес текста сообщения */
};
С каждым уникальным идентификатором очереди сообщений ассоциирована одна структура данных, которая содержит следующую информацию:
struct ipc_perm msg_perm; /* Структура прав на выполнение операций */
struct msg *msg_first; /* Указатель на первое сообщение в очереди */
struct msg *msg_last; /* Указатель на последнее сообщение в очереди */
ushort msg_cbytes; /* Текущее число байт в очереди */
ushort msg_qnum; /* Число сообщений в очереди */
ushort msg_qbytes; /* Макс. допустимое число байт в очереди */
ushort msg_lspid; /* Ид-р последнего отправителя */
ushort msg_lrpid; /* Ид-р последнего получателя */
time_t msg_stime; /* Время последнего отправления */
time_t msg_rtime; /* Время последнего получения */
time_t msg_ctime; /* Время последнего изменения */
};
ushort uid; /* Идентификатор пользователя */
ushort gid; /* Идентификатор группы */
ushort cuid; /* Идентификатор создателя очереди */
ushort cgid; /* Ид-р группы создателя очереди */
ushort mode; /* Права на чтение/запись */
ushort seq; /* Последовательность номеров используемых слотов */
key_t key; /* Ключ */
};
Если в аргументе msgflg системного вызова msgget установлен только флаг IPC_CREAT, выполняется одно из двух действий:
Кроме того, можно специфицировать ключ key со значением IPC_PRIVATE. Если указан такой ``личный'' ключ, для него обязательно выделяется новый уникальный идентификатор и создаются ассоциированные с ним очередь сообщений и структура данных (при условии, что это не приведет к превышению системного лимита). При выполнении утилиты ipcs поле KEY для подобного идентификатора msqid из соображений секретности содержит нули.
Если идентификатор msqid со специфицированным значением ключа key уже существует, выполняется второе действие, то есть возвращается ассоциированный идентификатор. Если необходимо считать возвращение существующего идентификатора ошибкой, в передаваемом системному вызову аргументе msgflg нужно установить флаг IPC_EXCL.
При выполнении первого действия процесс, вызвавший msgget, становится владельцем / создателем очереди сообщений; соответственно этому инициализируется ассоциированная структура данных. Напомним, что владелец очереди может быть изменен, однако процесс-создатель всегда остается создателем. При создании очереди сообщений определяются также начальные права на выполнение операций над ней.
После того, как созданы очередь сообщений с уникальным идентификатором и ассоциированная с ней структура данных, можно использовать системные вызовы семейства msgop (операции над очередями сообщений) и msgctl (управление очередями сообщений).
Операции, как упоминалось выше, заключаются в посылке и приеме сообщений. Для каждой из этих операций предусмотрен системный вызов, msgsnd() и msgrcv() соответственно.
Для управления очередями сообщений используется системный вызов msgctl. Он позволяет выполнять следующие управляющие действия:
В справочной статье для msgget синтаксис данного системного вызова описан так:
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget ( key_t key,int msgflg);
Целочисленное значение, возвращаемое в случае успешного завершения системного вызова, есть идентификатор очереди сообщений (msqid). В случае неудачи результат равен -1.
Новый идентификатор msqid, очередь сообщений и ассоциированная с ней структура данных выделяются в каждом из двух случаев:
Права на операции | Восьмеричное значение |
В каждом конкретном случае нужная комбинация прав задается как результат побитного ИЛИ значений, соответствующих элементарным правам. Так, правам на чтение / запись для владельца и на чтение для членов группы и прочих пользователей соответствует восьмеричное число 0644. Следует отметить полную аналогию с правами доступа к файлам.
Флаги определены во включаемом файле <sys/ipc.h>. В табл. 3 сведены мнемонические имена флагов и соответствующие им восьмеричные значения:
Флаг | Восьмеричное значение |
Значение аргумента msgflg в целом является, следовательно, результатом побитного ИЛИ (операция | в языке C) прав на выполнение операций и флагов, например:
msqid = msgget (key, (IPC_CREAT | IPC_EXCL | 0400));
При использовании флага IPC_EXCL в сочетании с IPC_CREAT системный вызов msgget завершается неудачей в том и только в том случае, когда с указанным ключом key уже ассоциирован идентификатор. Флаг IPC_EXCL необходим, чтобы предотвратить ситуацию, когда процесс полагает, что получил новый (уникальный) идентификатор очереди сообщений, хотя это не так. Иными словами, когда используются и IPC_CREAT и IPC_EXCL, при успешном завершении системного вызова обязательно возвращается новый идентификатор msqid.
В справочной статье по msgget описывается начальное значение ассоциированной структуры данных, формируемое при успешном завершении системного вызова. В статье содержится перечень условий, приводящих к ошибкам, и соответствующих им мнемонических имен для значений переменной errno.
Программа-пример для msgget (рис. 17) управляется посредством меню. Она позволяет поупражняться со всевозможными комбинациями в использовании системного вызова msgget, проследить, как передаются аргументы и получаются результаты. Имена переменных выбраны максимально близкими к именам, используемым в спецификации синтаксиса системного вызова, что облегчает чтение программы.
Выполнение программы начинается с приглашения ввести шестнадцатеричный ключ key, восьмеричный код прав на операции и, наконец, выбираемую при помощи меню комбинацию флагов. В меню предлагаются все возможные комбинации, даже бессмысленные, что позволяет при желании проследить за реакцией на ошибку. Затем выбранные флаги комбинируются с правами на операции, после чего выполняется системный вызов, результат которого помещается в переменную msqid. Если значение msqid равно -1, выдается сообщение об ошибке и выводится значение внешней переменной errno. Если ошибки не произошло, выводится значение полученного идентификатора очереди сообщений.
возможности системного вызова msgget()
(получение идентификатора очереди сообщений) */
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
main ()
{
key_t key; /* Тип описан как целое */
int opperm, flags; /* Права на операции и флаги */
int msgflg, msqid;
/* Ввести требуемый ключ */
printf ("\nВведите шестнадцатеричный ключ: ");
scanf ("%x", &key);
/* Ввести права на операции */
printf ("\nВведите права на операции ");
printf ("в восьмеричной записи: ");
scanf ("%o", &opperm);
/* Установить требуемые флаги */
printf ("\nВведите код, соответствущий ");
printf ("нужной комбинации флагов:\n");
printf (" Нет флагов = 0\n");
printf (" IPC_CREAT = 1\n");
printf (" IPC_EXCL = 2\n");
printf (" IPC_CREAT и IPC_EXCL = 3\n");
printf (" Выбор = ");
/* Получить флаги, которые нужно установить */
scanf ("%d", &flags);
/* Проверить значения */
printf ("\nключ = 0x%x, права = 0%o, флаги = %d\n",
key, opperm, flags);
/* Объединить флаги с правами на операции */
switch (flags) {
case 0: /* Флаги не устанавливать */
msgflg = (opperm | 0);
break;
case 1: /* Установить флаг IPC_CREAT */
msgflg = (opperm | IPC_CREAT);
break;
case 2: /* Установить флаг IPC_EXCL */
msgflg = (opperm | IPC_EXCL);
break;
case 3: /* Установить оба флага */
msgflg = (opperm | IPC_CREAT | IPC_EXCL);
}
/* Выполнить системный вызов msgget */
msqid = msgget (key, msgflg);
if (msqid == -1) {
/* Сообщить о неудачном завершении */
printf ("\nmsgget завершился неудачей!\n"
printf ("Код ошибки = %d\n", errno);
}
else
/* При успешном завершении сообщить msqid */
printf ("\nИдентификатор msqid = %d\n", msqid);
exit (0);
}
В справочной статье msgctl синтаксис данного системного вызова описан так:
#include <sys/ipc.h>
#include <sys/msg.h>
int msgctl ( int msqid, int cmd, struct msqid_ds *buf);
В качестве аргумента msqid должен выступать идентификатор очереди сообщений, предварительно полученный при помощи системного вызова msgget.
Управляющее действие определяется значением аргумента cmd. Допустимых значений три:
Поместить информацию о состоянии очереди, содержащуюся в структуре данных, ассоциированной с идентификатором msqid, в пользовательскую структуру, на которую указывает аргумент buf.
В структуре данных, ассоциированной с идентификатором msqid, переустановить значения действующих идентификаторов пользователя и группы, прав на операции, максимально допустимого числа байт в очереди.
Удалить из системы идентификатор msqid, ликвидировать очередь сообщений и ассоциированную с ней структуру данных.
Ниже приведена программа-пример (рис. 18), иллюстрирующая управление очередью. В программе использованы следующие переменные:
Если выбрано действие IPC_STAT (код 1), выполняется системный вызов и распечатывается информация о состоянии очереди; в программе распечатываются только те поля структуры, которые могут быть переустановлены. Если системный вызов завершается неудачей, распечатывается информация о состоянии очереди на момент последнего успешного выполнения системного вызова. Кроме того, выводится сообщение об ошибке и распечатывается значение переменной errno. Если системный вызов завершается успешно, выводится сообщение, уведомляющее об этом, и значение использованного идентификатора очереди сообщений.
Если выбрано действие IPC_SET (код 2), программа прежде всего получает информацию о текущем состоянии очереди сообщений с заданным идентификатором. Это необходимо, поскольку пример обеспечивает изменение только одного поля за один раз, в то время как системный вызов изменяет всю структуру целиком. Кроме того, если в одно из полей структуры, находящейся в области памяти пользователя, будет занесено некорректное значение, это может вызвать неудачи в выполнении управляющих действий, повторяющиеся до тех пор, пока значение поля не будет исправлено. Затем программа предлагает ввести код, соответствующий полю структуры, которое должно быть изменено. Этот код заносится в переменную choice. Далее, в зависимости от указанного поля, программа предлагает ввести то или иное новое значение. Значение заносится в соответствующее поле структуры данных, расположенной в области памяти пользователя, и выполняется системный вызов.
Если выбрано действие IPC_RMID (код 3), выполняется системный вызов, удаляющий из системы идентификатор msqid, очередь сообщений и ассоциированную с ней структуру данных. Отметим, что для выполнения этого управляющего действия аргумент buf не требуется, поэтому его значение может быть заменено нулем (NULL).
возможности системного вызова msgctl()
(управление очередями сообщений) */
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
main ()
{
extern int errno;
int msqid, command, choice, rtrn;
struct msqid_ds msqid_ds, *buf;
buf = &msqid_ds;
/* Ввести идентификатор и действие */
printf ("Введите идентификатор msqid: ");
scanf ("%d", &msqid);
printf ("Введите номер требуемого действия:\n");
printf (" IPC_STAT = 1\n");
printf (" IPC_SET = 2\n");
printf (" IPC_RMID = 3\n");
printf (" Выбор = ");
scanf ("%d", &command);
/* Проверить значения */
printf ("идентификатор = %d, действие = %d\n",
msqid, command);
switch (command) {
case 1: /* Скопировать информацию
о состоянии очереди сообщений
в пользовательскую структуру
и вывести ее */
rtrn = msgctl (msqid, IPC_STAT, buf);
printf ("\n Идентификатор пользователя = %d\n",
buf->msg_perm.uid);
printf ("\n Идентификатор группы = %d\n",
buf->msg_perm.gid);
printf ("\n Права на операции = 0%o\n",
buf->msg_perm.mode);
printf ("\n Размер очереди в байтах = %d\n",
buf->msg_qbytes);
break;
case 2: /* Выбрать и изменить поле (поля)
ассоциированной структуры данных */
/* Сначала получить исходное значение
структуры данных */
rtrn = msgctl (msqid, IPC_STAT, buf);
printf ("\nВведите номер поля, ");
printf ("которое нужно изменить:\n");
printf (" msg_perm.uid = 1\n");
printf (" msg_perm.gid = 2\n");
printf (" msg_perm.mode = 3\n");
printf (" msg_qbytes = 4\n");
printf (" Выбор = ");
scanf ("%d", &choice);
switch (choice) {
case 1:
printf ("\nВведите ид-р пользователя: ");
scanf ("%d", &buf->msg_perm.uid);
printf ("\nИд-р пользователя = %d\n",
buf->msg_perm.uid);
break;
case 2:
printf ("\nВведите ид-р группы: ");
scanf ("%d", &buf->msg_perm.gid);
printf ("\nИд-р группы = %d\n",
buf->msg_perm.uid);
break;
case 3:
printf ("\nВведите восьмеричный код прав: ");
scanf ("%o", &buf->msg_perm.mode);
printf ("\nПрава на операции = 0%o\n",
buf->msg_perm.mode);
break;
case 4:
printf ("\nВведите размер очереди = ");
scanf ("%d", &buf->msg_qbytes);
printf ("\nЧисло байт в очереди = %d\n",
buf->msg_qbytes);
break;
}
/* Внести изменения */
rtrn = msgctl (msqid, IPC_SET, buf);
break;
case 3: /* Удалить идентификатор и
ассоциированные с ним очередь
сообщений и структуру данных */
rtrn = msgctl (msqid, IPC_RMID, NULL);
}
if (rtrn == -1) {
/* Сообщить о неудачном завершении */
printf ("\nmsgctl завершился неудачей!\n");
printf ("\nКод ошибки = %d\n", errno);
}
else {
/* При успешном завершении сообщить msqid */
printf ("\nmsgctl завершился успешно,\n");
printf ("идентификатор = %d\n", msqid);
}
exit (0);
}
В справочной статье msgop синтаксис упомянутых системных вызовов описан так:
#include <sys/ipc.h>
#include <sys/msg.h>
int msgsnd (int msqid, struct msgbuf * msgp, int msgsz, int msgflg)
int msgrcv (int msqid, struct msgbuf * msgp, int msgsz, long msgtyp,
int msgflg)
Отметим, что значение поля msg_qbytes у ассоциированной структуры данных может быть уменьшено с предполагаемой по умолчанию величины MSGMNB при помощи управляющего действия IPC_SET системного вызова msgctl, однако впоследствии увеличить его может только суперпользователь. Аргумент msgflg позволяет специфицировать выполнение над сообщением ``операции с блокировкой''; для этого флаг IPC_NOWAIT должен быть сброшен (msgflg & IPC_NOWAIT = 0). Блокировка имеет место, если либо текущее число байт в очереди уже равно максимально допустимому значению для указанной очереди (то есть значению поля msg_qbytes или MSGMNB), либо общее число сообщений во всех очередях равно максимально допустимому системой (системный параметр MSGTQL). Если в такой ситуации флаг IPC_NOWAIT установлен, системный вызов msgsnd() завершается неудачей и возвращает -1.
При успешном завершении системного вызова msgrcv() результат равен числу принятых байт; в случае неудачи возвращается -1. В качестве аргумента msqid должен выступать идентификатор очереди сообщений, предварительно полученный при помощи системного вызова msgget. Аргумент msgp является указателем на структуру в области памяти пользователя, содержащую тип принимаемого сообщения и его текст. Аргумент msgsz специфицирует длину принимаемого сообщения. Можно указать, что в случае, если значение данного аргумента меньше, чем длина сообщения в массиве, должна возникать ошибка (см. описание аргумента msgflg).
Аргумент msgtyp используется для выбора из очереди первого сообщения определенного типа. Если значение аргумента равно нулю, запрашивается первое сообщение в очереди, если больше нуля - первое сообщение типа msgtyp, а если меньше нуля - первое сообщение наименьшего из типов, которые не превосходят абсолютной величины аргумента msgtyp.
Аргумент msgflg позволяет специфицировать выполнение над сообщением ``операции с блокировкой''; для этого должен быть сброшен флаг IPC_NOWAIT (msgflg & IPC_NOWAIT = 0). Блокировка имеет место, если в очереди сообщений нет сообщения с запрашиваемым типом (msgtyp). Если флаг IPC_NOWAIT установлен и в очереди нет сообщения требуемого типа, системный вызов немедленно завершается неудачей. Аргумент msgflg может также специфицировать, что системный вызов должен заканчиваться неудачей, если размер сообщения в очереди больше значения msgsz; для этого в данном аргументе должен быть сброшен флаг MSG_NOERROR (msgflg & MSG_NOERROR = 0). Если флаг MSG_NOERROR установлен, сообщение обрезается до длины, указанной аргументом msgsz.
В приведенном ниже примере (рис. 19) используются следующие переменные:
Если выбрана операция посылки сообщения, указатель msgp инициализируется адресом структуры данных sndbuf. После этого запрашивается идентификатор очереди сообщений, в которую должно быть послано сообщение; идентификатор заносится в переменную msqid. Затем должен быть введен тип сообщения; он заносится в поле mtype структуры данных, указываемой значением msgp.
После этого программа приглашает ввести с клавиатуры текст посылаемого сообщения и выполняет цикл, в котором символы читаются и заносятся в массив mtext структуры данных. Ввод продолжается до тех пор, пока не будет обнаружен признак конца файла; для функции getchar() таким признаком является символ CTRL+D, непосредственно следующий за символом возврата каретки. После того как признак конца обнаружен, определяется размер сообщения - он на единицу больше значения счетчика i, поскольку элементы массива, в который заносится сообщение, нумеруются с нуля. Следует помнить, что сообщение будет содержать заключительные символы и, следовательно, будет казаться, что сообщение на три символа короче, чем указывает аргумент msgsz.
Чтобы обеспечить пользователю обратную связь, текст сообщения, содержащийся в массиве mtext структуры sndbuf, немедленно выводится на экран.
Следующее, и последнее, действие заключается в определении, должен ли быть установлен флаг IPC_NOWAIT. Чтобы выяснить это, программа предлагает ввести 1, если флаг нужно установить, или любое другое число, если он не нужен. Введенное значение заносится в переменную flag. Если введена единица, аргумент msgflg полагается равным IPC_NOWAIT, в противном случае msgflg устанавливается равным нулю.
После этого выполняется системный вызов msgsnd(). Если вызов завершается неудачей, выводится сообщение об ошибке, а также ее код. Если вызов завершается успешно, печатается возвращенное им значение, которое должно быть равно нулю.
При каждой успешной посылке сообщения обновляются три поля ассоциированной структуры данных. Изменения можно описать следующим образом:
Если указано, что требуется принять сообщение, начальное значение указателя msgp устанавливается равным адресу структуры данных rcvbuf. Запрашивается код требуемой комбинации флагов, который заносится в переменную flags. Переменная msgflg устанавливается в сответствии с выбранной комбинацией. В заключение запрашивается, сколько байт нужно принять; указанное значение заносится в переменную msgsz. После этого выполняется системный вызов msgrcv().
Если вызов завершается неудачей, выводится сообщение об ошибке, а также ее код. Если вызов завершается успешно, программа сообщает об этом, а также выводит размер и текст сообщения. При каждом успешном приеме сообщения обновляются три поля ассоциированной структуры данных. Изменения можно описать следующим образом:
msg_qnum - Определяет общее число сообщений в очереди; в результате выполнения операции уменьшается на единицу.
msg_lrpid - Содержит идентификатор процесса, который последним получил сообщение; полю присваивается соответствующий идентификатор.
msg_rtime - Содержит время последнего получения сообщения, время измеряется в секундах, начиная с 00:00:00 1 января 1970 года (по Гринвичу).
возможности системных вызовов msgsnd() и msgrcv()
(операции над очередями сообщений) */
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
#define MAXTEXTSIZE 8192
struct msgbufl {
long mtype;
char mtext [MAXTEXTSIZE];
} sndbuf, rcvbuf, *msgp;
main ()
{
extern int errno;
int flag, flags, choice, rtrn, i, c;
int rtrn, msqid, msgsz, msgflg;
long msgtyp;
struct msqid_ds msqid_ds, *buf;
buf = &msqid_ds;
/* Выбрать требуемую операцию */
printf ("\nВведите код, соответствующий ");
printf ("посылке или приему сообщения:\n");
printf (" Послать = 1\n");
printf (" Принять = 2\n");
printf (" Выбор = ");
scanf ("%d", &choice);
if (choice == 1) {
/* Послать сообщение */
msgp = &sndbuf; /* Указатель на структуру */
printf ("\nВведите идентификатор ");
printf ("очереди сообщений,\n");
printf ("в которую посылается сообщение: ");
scanf ("%d", &msqid);
/* Установить тип сообщения */
printf ("\nВведите положительное число - ");
printf ("тип сообщения: ");
scanf ("%d", &msgp->mtype);
/* Ввести посылаемое сообщение */
printf ("\nВведите сообщение: \n");
/* Управляющая последовательность CTRL+D
завершает ввод сообщения */
/* Прочитать символы сообщения
и поместить их в массив mtext */
for (i = 0; ((c = getchar ()) != EOF); i++)
sndbuf.mtext [i] = c;
/* Определить размер сообщения */
msgsz = i + 1;
/* Выдать текст посылаемого сообщения */
for (i = 0; i < msgsz; i++)
putchar (sndbuf.mtext [i]);
/* Установить флаг IPC_NOWAIT, если это нужно */
printf ("\nВведите 1, если хотите установить ");
printf ("флаг IPC_NOWAIT: ");
scanf ("%d", &flag);
if (flag == 1) msgflg = IPC_NOWAIT;
else msgflg = 0;
/* Проверить флаг */
printf ("\nФлаг = 0%o\n", msgflg);
/* Послать сообщение */
rtrn = msgsnd (msqid, msgp, msgsz, msgflg);
if (rtrn == -1) {
printf ("\nmsgsnd завершился неудачей!\n");
printf ("Код ошибки = %d\n", errno);
}
else {
/* Вывести результат; при успешном
завершении он должен равняться нулю */
printf ("\nРезультат = %d\n", rtrn);
/* Вывести размер сообщения */
printf ("\nРазмер сообщения = %d\n", msgsz);
/* Опрос измененной структуры данных */
msgctl (msqid, IPC_STAT, buf);
/* Вывести изменившиеся поля */
printf ("Число сообщений в очереди = %d\n",
buf->msg_qnum);
printf ("Ид-р последнего отправителя = %d\n",
buf->msg_lspid);
printf ("Время последнего отправления = %d\n",
buf->msg_stime);
}
}
if (choice == 2) {
/* Принять сообщение */
msgp = &rcvbuf;
/* Определить нужную очередь сообщений */
printf ("\nВведите ид-р очереди сообщений: ");
scanf ("%d", &msqid);
/* Определить тип сообщения */
printf ("\nВведите тип сообщения: ");
scanf ("%d", &msgtyp);
/* Сформировать управляющие флаги
для требуемых действий */
printf ("\nВведите код, соответствущий ");
printf ("нужной комбинации флагов:\n");
printf (" Нет флагов = 0\n");
printf (" MSG_NOERROR = 1\n");
printf (" IPC_NOWAIT = 2\n");
printf (" MSG_NOERROR и IPC_NOWAIT = 3\n");
printf (" Выбор = ");
scanf ("%d", &flags);
switch (flags) {
/* Установить msgflg как побитное ИЛИ
соответствующих констант */
case 0:
msgflg = 0;
break;
case 1:
msgflg = MSG_NOERROR;
break;
case 2:
msgflg = IPC_NOWAIT;
break;
case 3:
msgflg = MSG_NOERROR | IPC_NOWAIT;
break;
}
/* Определить, какое число байт принять */
printf ("\nВведите число байт, которое ");
printf ("нужно принять (msgsz): ");
scanf ("%d", &msgsz);
/* Проверить значение аргументов */
printf ("\nИдентификатор msqid = %d\n", msqid);
printf ("Тип сообщения = %d\n", msgtyp);
printf ("Число байт = %d\n", msgsz);
printf ("Флаги = %o\n", msgflg);
/* Вызвать msgrcv для приема сообщения */
rtrn = msgrcv (msqid, msgp, msgsz, msgtyp, msgflg);
if (rtrn == -1) {
printf ("\nmsgrcv завершился неудачей!\n");
printf ("Код oшибки = %d\n", errno);
}
else {
printf ("\nmsgrcv завершился успешно,\n");
printf ("идентификатор очереди = %d\n", msqid);
/* Напечатать число принятых байт,
оно равно возвращаемому значению */
printf ("Принято байт: %d\n", rtrn);
/* Распечатать принятое сообщение */
for (i = 0; i < rtrn; i++) putchar (rcvbuf.mtext [i]);
}
/* Опрос ассоциированной структуры данных */
msgctl (msqid, IPC_STAT, buf);
printf ("\nЧисло сообщений в очереди = %d\n",
buf->msg_qnum);
printf ("Ид-р последнего получателя = %d\n",
buf->msg_lrpid);
printf ("Время последнего получения = %d\n",
buf->msg_rtime);
}
exit (0);
}
Семафоры являются одним из классических примитивов синхронизации. Значение семафора - это целое число в диапазоне от 0 до 32767. Поскольку во многих приложениях требуется более одного семафора, ОС UNIX предоставляет возможность создавать множества семафоров. Их максимальный размер ограничен системным параметром SEMMSL. Множества семафоров создаются при помощи системного вызова semget.
Процесс, выполнивший системный вызов semget, становится владельцем / создателем множества семафоров. Он определяет, сколько будет семафоров в множестве; кроме того, он специфицирует первоначальные права на выполнение операций над множеством для всех процессов, включая себя. Впоследствии данный процесс может уступить право собственности или изменить права на операции при помощи системного вызова semctl, предназначенного для управления семафорами, однако на протяжении всего времени существования множества семафоров создатель остается создателем. Другие процессы, обладающие соответствующими правами, для выполнения прочих управляющих действий также могут использовать системный вызов semctl.
Над каждым семафором, принадлежащим некоторому множеству, при помощи системного вызова semop можно выполнить любую из трех операций:
Операции могут снабжаться флагами. Флаг SEM_UNDO означает, что операция выполняется в проверочном режиме, то есть требуется только узнать, можно ли успешно выполнить данную операцию.
При отсутствии флага IPC_NOWAIT системный вызов semop может быть приостановлен до тех пор, пока значение семафора, благодаря действиям другого процесса, не позволит успешно завершить операцию (ликвидация множества семафоров также приведет к завершению системного вызова). Подобные операции называются ''операциями с блокировкой''. С другой стороны, если обработка завершается неудачей и не указано, что выполнение процесса должно быть приостановлено, операция над семафором называется ``операцией без блокировки''.
Системный вызов semop оперирует не с отдельным семафором, а с множеством семафоров, применяя к нему ``массив операций''. Массив содержит информацию о том, с какими семафорами нужно оперировать и каким образом. Выполнение массива операций с точки зрения пользовательского процесса является неделимым действием. Это значит, во-первых, что если операции выполняются, то только все вместе и, во-вторых, что другой процесс не может получить доступ к промежуточному состоянию множества семафоров, когда часть операций из массива уже выполнилась, а другая часть еще не успела.
Операционная система выполняет операции из массива по очереди, причем порядок не оговаривается. Если очередная операция не может быть выполнена, то эффект предыдущих операций аннулируется. Если таковой оказалась операция с блокировкой, выполнение системного вызова приостанавливается. Если неудачу потерпела операция без блокировки, системный вызов немедленно завершается, возвращая значение -1 как признак ошибки, а внешней переменной errno присваивается код ошибки.
Перед тем как использовать семафоры (выполнять операции или управляющие действия), нужно создать множество семафоров с уникальным идентификатором и ассоциированной структурой данных. Уникальный идентификатор называется идентификатором множества семафоров (semid); он используется для обращений к множеству и структуре данных. С точки зрения реализации множество семафоров представляет собой массив структур. Каждая структура соответствует семафору и определяется следующим образом:
ushort semval; /* Значение семафора */
short sempid; /* Идентификатор процесса, выполнявшего последнюю
операцию */
ushort semncnt; /* Число процессов, ожидающих увеличения значения
семафора */
ushort semzcnt; /* Число процессов, ожидающих обнуления значения
семафора */
};
С каждым идентификатором множества семафоров ассоциирована структура данных, содержащая следующую информацию:
struct ipc_perm sem_perm; /* Структура прав на выполнение операций */
struct sem *sem_base; /* Указатель на первый семафор в множестве */
ushort sem_nsems; /* Количество семафоров в множестве */
time_t sem_otime; /* Время последней операции */
time_t sem_ctime; /* Время последнего изменения */
};
Поле sem_perm данной структуры использует в качестве шаблона структуру типа ipc_perm, общую для всех средств межпроцессной связи. Системный вызов semget аналогичен вызову msgget (разумеется, с заменой слов ``очередь сообщений'' на ``множество семафоров''). Он также предназначен для получения нового или опроса существующего идентификатора, а нужное действие определяется значением аргумента key. В аналогичных ситуациях semget терпит неудачу. Единственное отличие состоит в том, что при создании требуется посредством аргумента nsems указывать число семафоров в множестве.
После того как созданы множество семафоров с уникальным идентификатором и ассоциированная с ним структура данных, можно использовать системные вызовы semop для операций над семафорами и semctl для выполнения управляющих действий.
Для создания множества семафоров служит системный вызов semget. В справочной статье semget синтаксис данного системного вызова описан так:
#include <sys/ipc.h>
#include <sys/sem.h>
int semget (key_t key, int nsems, int semflg);
Смысл аргументов key и semflg тот же, что и у соответствующих аргументов системного вызова msgget. Аргумент nsems задает число семафоров в множестве. Если запрашивается идентификатор существующего множества, значение nsems не должно превосходить числа семафоров в множестве.
Превышение системных параметров SEMMNI, SEMMNS и SEMMSL при попытке создать новое множество всегда ведет к неудачному завершению. Системный параметр SEMMNI определяет максимально допустимое число уникальных идентификаторов множеств семафоров в системе. Системный параметр SEMMNS определяет максимальное общее число семафоров в системе. Системный параметр SEMMSL определяет максимально допустимое число семафоров в одном множестве.
В справочной статье semctl синтаксис данного системного вызова описан так:
#include <sys/ipc.h>
#include <sys/sem.h>
int semctl (int semid, int semnum, int cmd, arg);
union semun {
int val;
struct semid_ds *buf;
ushort *array;
} arg;
Аргументы semid и semnum определяют множество или отдельный семафор, над которым выполняется управляющее действие. В качестве аргумента semid должен выступать идентификатор множества семафоров, предварительно полученный при помощи системного вызова semget. Аргумент semnum задает номер семафора в множестве. Семафоры нумеруются с нуля.
Назначение аргумента arg зависит от управляющего действия, которое определяется значением аргумента cmd. Допустимы следующие действия:
Пример работы с семафорами (рис. 20):
возможности системного вызова semctl()
(управление семафорами) */
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define MAXSETSIZE 25
main ()
{
extern int errno;
struct semid_ds semid_ds;
int length, rtrn, i, c;
int semid, semnum, cmd, choice;
union semun {
int val;
struct semid_ds *buf;
ushort array [MAXSETSIZE];
} arg;
/* Инициализация указателя на структуру данных */
arg.buf = &semid_ds;
/* Ввести идентификатор множества семафоров */
printf ("Введите ид-р множества семафоров: ");
scanf ("%d", &semid);
/* Выбрать требуемое управляющее действие */
printf ("\nВведите номер требуемого действия:\n");
printf (" GETVAL = 1\n");
printf (" SETVAL = 2\n");
printf (" GETPID = 3\n");
printf (" GETNCNT = 4\n");
printf (" GETZCNT = 5\n");
printf (" GETALL = 6\n");
printf (" SETALL = 7\n");
printf (" IPC_STAT = 8\n");
printf (" IPC_SET = 9\n");
printf (" IPC_RMID = 10\n");
printf (" Выбор = ");
scanf ("%d", &cmd);
/* Проверить значения */
printf ("идентификатор = %d, команда = %d\n",
semid, cmd);
/* Сформировать аргументы и выполнить вызов */
switch (cmd) {
case 1: /* Получить значение */
printf ("\nВведите номер семафора: ");
scanf ("%d", &semnum);
/* Выполнить системный вызов */
rtrn = semctl (semid, semnum, GETVAL, 0);
printf ("\nЗначение семафора = %d\n", rtrn);
break;
case 2: /* Установить значение */
printf ("\nВведите номер семафора: ");
scanf ("%d", &semnum);
printf ("\nВведите значение: ");
scanf ("%d", &arg.val);
/* Выполнить системный вызов */
rtrn = semctl (semid, semnum, SETVAL, arg.val);
break;
case 3: /* Получить ид-р процесса */
rtrn = semctl (semid, 0, GETPID, 0);
printf ("\Последнюю операцию выполнил: %d\n",rtrn);
break;
case 4: /* Получить число процессов, ожидающих
увеличения значения семафора */
printf ("\nВведите номер семафора: ");
scanf ("%d", &semnum);
/* Выполнить системный вызов */
rtrn = semctl (semid, semnum, GETNCNT, 0);
printf ("\nЧисло процессов = %d\n", rtrn);
break;
case 5: /* Получить число процессов, ожидающих
обнуления значения семафора */
printf ("Введите номер семафора: ");
scanf ("%d", &semnum);
/* Выполнить системный вызов */
rtrn = semctl (semid, semnum, GETZCNT, 0
printf ("\nЧисло процессов = %d\n", rtrn);
break;
case 6: /* Опросить все семафоры */
/* Определить число семафоров в множестве */
rtrn = semctl (semid, 0, IPC_STAT, arg.buf);
length = arg.buf->sem_nsems;
if (rtrn == -1) goto ERROR;
/* Получить и вывести значения всех
семафоров в указанном множестве */
rtrn = semctl (semid, 0, GETALL, arg.array);
for (i = 0; i < length; i++)
printf (" %d", arg.array [i]);
break;
case 7: /* Установить все семафоры */
/* Определить число семафоров в множестве */
rtrn = semctl (semid, 0, IPC_STAT, arg.buf);
length = arg.buf->sem_nsems;
if (rtrn == -1) goto ERROR;
printf ("\nЧисло семафоров = %d\n", length);
/* Установить значения семафоров множества */
printf ("\nВведите значения:\n");
for (i = 0; i < length; i++)
scanf ("%d", &arg.array [i]);
/* Выполнить системный вызов */
rtrn = semctl (semid, 0, SETALL, arg.array);
break;
case 8: /* Опросить состояние множества */
rtrn = semctl (semid, 0, IPC_STAT, arg.buf);
printf ("\nИдентификатор пользователя = %d\n",
arg.buf->sem_perm.uid);
printf ("Идентификатор группы = %d\n",
arg.buf->sem_perm.gid);
printf ("Права на операции = 0%o\n",
arg.buf->sem_perm.mode);
printf ("Число семафоров в множестве = %d\n",
arg.buf->sem_nsems);
printf ("Время последней операции = %d\n",
arg.buf->sem_otime);
printf ("Время последнего изменения = %d\n",
arg.buf->sem_ctime);
break;
case 9: /* Выбрать и изменить поле
ассоциированной структуры данных */
/* Опросить текущее состояние */
rtrn = semctl (semid, 0, IPC_STAT, arg.buf);
if (rtrn == -1) goto ERROR;
printf ("\nВведите номер поля, ");
printf ("которое нужно изменить: \n");
printf (" sem_perm.uid = 1\n");
printf (" sem_perm.gid = 2\n");
printf (" sem_perm.mode = 3\n");
printf (" Выбор = ");
scanf ("%d", &choice);
switch (choice) {
case 1: /* Изменить ид-р владельца */
printf ("\nВведите ид-р владельца: ");
scanf ("%d", &arg.buf->sem_perm.uid);
printf ("\nИд-р владельца = %d\n",
arg.buf->sem_perm.uid);
break;
case 2: /* Изменить ид-р группы */
printf ("\nВведите ид-р группы = ");
scanf ("%d", &arg.buf->sem_perm.gid);
printf ("\nИд-р группы = %d\n",
arg.buf->sem_perm.uid);
break;
case 3: /* Изменить права на операции */
printf ("\nВведите восьмеричный код прав: ");
scanf ("%o", &arg.buf->sem_perm.mode);
printf ("\nПрава = 0%o\n",
arg.buf->sem_perm.mode);
break;
}
/* Внести изменения */
rtrn = semctl (semid, 0, IPC_SET, arg.buf);
break;
case 10: /* Удалить ид-р множества семафоров и
ассоциированную структуру данных */
rtrn = semctl (semid, 0, IPC_RMID, 0);
}
if (rtrn == -1) {
/* Сообщить о неудачном завершении */
ERROR:
printf ("\nsemctl завершился неудачей!\n");
printf ("\nКод ошибки = %d\n", errno);
}
else {
printf ("\nmsgctl завершился успешно,\n");
printf ("идентификатор semid = %d\n", semid);
}
exit (0);
}
В справочной статье semop синтаксис данного системного вызова описан так:
#include <sys/ipc.h>
#include <sys/sem.h>
int semop (int semid, struct sembuf *sops, unsigned int nsops)
В качестве аргумента semid должен выступать идентификатор множества семафоров, предварительно полученный при помощи системного вызова semget.
Аргумент sops (массив структур) определяет, над какими семафорами будут выполняться операции и какие именно. Структура, описывающая операцию над одним семафором, определяется следующим образом:
short sem_num; /* Номер семафора */
short sem_op; /* Операция над семафором */
short sem_flg; /* Флаги операции */
};
Номер семафора задает конкретный семафор в множестве, над которым должна быть выполнена операция.
Выполняемая операция определяется следующим образом:
Пример работы с семафорами приведен ниже (рис. 21):
возможности системного вызова semop()
(операции над множеством семафоров) */
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#define MAXOPSIZE 10
main ()
{
extern int errno;
struct sembuf sops [MAXOPSIZE];
int semid, flags, i, rtrn;
unsigned nsops;
/* Ввести идентификатор множества семафоров */
printf ("\nВведите идентификатор множества семафоров,");
printf ("\nнад которым будут выполняться операции: ");
scanf ("%d", &semid);
printf ("\nИд-р множества семафоров = %d", semid);
/* Ввести число операций */
printf ("\nВведите число операций ");
printf ("над семафорами из этого множества: \n");
scanf ("%d", &nsops);
printf ("\nЧисло операций = %d", nsops);
/* Инициализировать массив операций */
for (i = 0; i < nsops; i++) {
/* Выбрать семафор из множества */
printf ("\nВведите номер семафора: ");
scanf ("%d", &sops [i].sem_num);
printf ("\nНомер = %d", sops [i].sem_num);
/* Ввести число, задающее операцию */
printf ("\nЗадайте операцию над семафором: ");
scanf ("%d", &sops [i].sem_op);
printf ("\nОперация = %d", sops [i].sem_op);
/* Указать требуемые флаги */
printf ("\nВведите код, ");
printf ("соответствующий требуемым флагам:\n");
printf (" Нет флагов = 0\n");
printf (" IPC_NOWAIT = 1\n");
printf (" SEM_UNDO = 2\n");
printf (" IPC_NOWAIT и SEM_UNDO = 3\n");
printf (" Выбор = ");
scanf ("%d", &flags);
switch (flags) {
case 0:
sops [i].sem_flg = 0;
break;
case 1:
sops [i].sem_flg = IPC_NOWAIT;
break;
case 2:
sops [i].sem_flg = SEM_UNDO;
break;
case 3:
sops [i].sem_flg = IPC_NOWAIT | SEM_UNDO;
break;
}
printf ("\nФлаги = 0%o", sops [i].sem_flg);
}
/* Распечатать все структуры массива */
printf ("\nМассив операций:\n");
for (i = 0; i < nsops; i++) {
printf (" Номер семафора = %d\n", sops [i].sem_num);
printf (" Операция = %d\n", sops [i].sem_op);
printf (" Флаги = 0%o\n", sops [i].sem_flg);
}
/* Выполнить системный вызов */
rtrn = semop (semid, sops, nsops);
if (rtrn == -1) {
printf ("\nsemop завершился неудачей!\n");
printf ("Код ошибки = %d\n", errno);
}
else {
printf ("\nsemop завершился успешно.\n");
printf ("Идентификатор semid = %d\n", semid);
printf ("Возвращенное значение = %d\n", rtrn);
}
exit (0);
}
Разделяемые сегменты памяти как средство межпроцессной связи позволяют процессам иметь общие области виртуальной памяти и, как следствие, разделять содержащуюся в них информацию. Единицей разделяемой памяти являются сегменты, свойства которых зависят от аппаратных особенностей управления памятью.
Разделение памяти обеспечивает наиболее быстрый обмен данными между процессами.
Работа с разделяемой памятью начинается с того, что процесс при помощи системного вызова shmget создает разделяемый сегмент, специфицируя первоначальные права доступа к сегменту (чтение и / или запись) и его размер в байтах. Чтобы затем получить доступ к разделяемому сегменту, его нужно присоединить посредством системного вызова shmat(), который разместит сегмент в виртуальном пространстве процесса. После присоединения, в соответствии с правами доступа, процессы могут читать данные из сегмента и записывать их (возможно, синхронизируя свои действия с помощью семафоров).
Когда разделяемый сегмент становится ненужным, его следует отсоединить, воспользовавшись системным вызовом shmdt().
Для выполнения управляющих действий над разделяемыми сегментами памяти служит системный вызов shmctl(). В число управляющих действий входит предписание удерживать сегмент в оперативной памяти и обратное предписание о снятии удержания. После того, как последний процесс отсоединил разделяемый сегмент, следует выполнить управляющее действие по удалению сегмента из системы.
Прежде чем воспользоваться разделением памяти, нужно создать разделяемый сегмент с уникальным идентификатором и ассоциированную с ним структуру данных. Уникальный идентификатор называется идентификатором разделяемого сегмента памяти (shmid); он используется для обращений к ассоциированной структуре данных, которая определяется следующим образом:
struct ipc_perm shm_perm; /* Структура прав на выполнение операций */
int shm_segsz; /* Размер сегмента */
struct region *shm_reg; /* Указатель на структуру области памяти */
char pad [4]; /* Информация для подкачки */
ushort shm_lpid; /* Ид-р процесса, вып. последнюю операцию */
ushort shm_cpid; /* Ид-р процесса, создавшего сегмент */
ushort shm_nattch; /* Число присоединивших сегмент */
ushort shm_cnattch; /* Число удерживающих сегмент в памяти */
time_t shm_atime; /* Время последнего присоединения */
time_t shm_dtime; /* Время последнего отсоединения */
time_t shm_ctime; /* Время последнего изменения */
};
Табл. 4 содержит информацию о возможных состояниях разделяемых сегментов памяти:
Бит удержания | Бит подкачки | Бит размещения | Состояние |
Состояния, упомянутые в таблице, таковы:
После того, как создан уникальный идентификатор разделяемого сегмента памяти и ассоциированная с ним структура данных, можно использовать системные вызовы семейства shmop (операции над разделяемыми сегментами) и shmctl (управление разделяемыми сегментами).
Для создания разделяемого сегмента памяти служит системный вызов shmget. Синтаксис данного системного вызова описан так:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmget (key_t key, int size, int shmflg);
Смысл аргументов key и shmflg тот же, что и у соответствующих аргументов системного вызова semget. Аргумент size задает размер разделяемого сегмента в байтах.
Системный параметр SHMMNI определяет максимально допустимое число уникальных идентификаторов разделяемых сегментов памяти (shmid) в системе. Попытка его превышения ведет к неудачному завершению системного вызова.
Системный вызов завершится неудачей и тогда, когда значение аргумента size меньше, чем SHMMIN, либо больше, чем SHMMAX. Данные системные параметры определяют, соответственно, минимальный и максимальный размеры разделяемого сегмента памяти.
В справочной статье shmctl синтаксис данного системного вызова описан так:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmctl (int shmid, int cmd, struct shmid_ds *buf);
В качестве аргумента shmid должен выступать идентификатор разделяемого сегмента памяти, предварительно полученный при помощи системного вызова shmget.
Управляющее действие определяется значением аргумента cmd. Допустимы следующие
значения:
Управляющие действия SHM_LOCK и SHM_UNLOCK может выполнить только суперпользователь. Для выполнения управляющего действия IPC_STAT процессу требуется право на чтение (рис. 22).
возможности системного вызова shmctl()
(операции управления разделяемыми сегментами) */
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
main ()
{
extern int errno;
int rtrn, shmid, command, choice;
struct shmid_ds shmid_ds, *buf;
buf = &shmid_ds;
/* Ввести идентификатор сегмента и действие */
printf ("Введите идентификатор shmid: ");
scanf ("%d", &shmid);
printf ("Введите номер требуемого действия:\n");
printf (" IPC_STAT = 1\n");
printf (" IPC_SET = 2\n");
printf (" IPC_RMID = 3\n");
printf (" SHM_LOCK = 4\n");
printf (" SHM_UNLOCK = 5\n");
printf (" Выбор = ");
scanf ("%d", &command);
/* Проверить значения */
printf ("\nидентификатор = %d, действие = %d\n",
shmid, command);
switch (command) {
case 1: /* Скопировать информацию
о состоянии разделяемого сегмента
в пользовательскую структуру
и вывести ее */
rtrn = shmctl (shmid, IPC_STAT, buf);
printf ("\nИд-р пользователя = %d\n",
buf->shm_perm.uid);
printf ("Ид-р группы пользователя = %d\n",
buf->shm_perm.gid);
printf ("Ид-р создателя = %d\n",
buf->shm_perm.cuid);
printf ("Ид-р группы создателя = %d\n",
buf->shm_perm.cgid);
printf ("Права на операции = 0%o\n",
buf->shm_perm.mode);
printf ("Последовательность номеров ");
buf->shm_perm.cgid);
printf ("используемых слотов = 0%x\n",
buf->shm_perm.seq);
printf ("Ключ = 0%x\n", buf->shm_perm.key);
printf ("Размер сегмента = %d\n", buf->shm_segsz);
printf ("Выполнил последнюю операцию = %d\n",
buf->shm_lpid);
printf ("Создал сегмент = %d\n", buf->shm_cpid);
printf ("Число присоединивших сегмент = %d\n",
buf->shm_nattch);
printf ("Число удерживаюших в памяти = %d\n",
buf->shm_cnattch);
printf ("Последнее присоединение = %d\n",
buf->shm_atime);
printf ("Последнее отсоединение = %d\n",
buf->shm_dtime);
printf ("Последнее изменение = %d\n",
buf->shm_ctime);
break;
case 2: /* Выбрать и изменить поле (поля)
ассоциированной структуры данных */
/* Получить исходные значения структуры данных */
rtrn = shmctl (shmid, IPC_STAT, buf);
printf ("Введите номер изменяемого поля:\n");
printf (" shm_perm.uid = 1\n");
printf (" shm_perm.gid = 2\n");
printf (" shm_perm.mode = 3\n");
printf (" Выбор = ");
scanf ("%d", &choice);
switch (choice) {
case 1:
printf ("\nВведите ид-р пользователя:"),
scanf ("%d", &buf->shm_perm.uid);
printf ("\nИд-р пользователя = %d\n",
buf->shm_perm.uid);
break;
case 2:
printf ("\nВведите ид-р группы: "),
scanf ("%d", &buf->shm_perm.gid);
printf ("\nИд-р группы = %d\n",
buf->shm_perm.uid);
break;
case 3:
printf ("\nВведите восьмеричный
код прав: ");
scanf ("%o", &buf->shm_perm.mode);
printf ("\nПрава на операции
= 0%o\n",
buf->shm_perm.mode);
break;
}
/* Внести изменения */
rtrn = shmctl (shmid, IPC_SET, buf);
break;
case 3: /* Удалить идентификатор и
ассоциированную структуру данных */
rtrn = shmctl (shmid, IPC_RMID, NULL);
break;
case 4: /* Удерживать разделяемый сегмент
в памяти */
rtrn = shmctl (shmid, SHM_LOCK, NULL);
break;
case 5: /* Перестать удерживать сегмент в памяти */
rtrn = shmctl (shmid, SHM_UNLOCK, NULL);
}
if (rtrn == -1) {
/* Сообщить о неудачном завершении */
printf ("\nshmctl завершился неудачей!\n");
printf ("\nКод ошибки = %d\n", errno);
}
else {
/* При успешном завершении сообщить ид-р shmid */
printf ("\nshmctl завершился успешно, ");
printf ("идентификатор shmid = %d\n", shmid);
}
exit (0);
}
В справочной статье shmop синтаксис системных вызовов shmat и shmdt описан так:
#include <sys/ipc.h>
#include <sys/shm.h>
int shmat (int shmid, char *shmaddr, int shmflg);
int shmdt (char *shmaddr);
Разумеется, чтобы использовать результат shmat() как указатель, его нужно преобразовать к требуемому типу.
В качестве аргумента shmid должен выступать идентификатор разделяемого сегмента, предварительно полученный при помощи системного вызова shmget. Аргумент shmaddr задает адрес, по которому сегмент должен быть присоединен, то есть тот адрес в виртуальном пространстве пользователя, который получит начало сегмента. Не всякий адрес является приемлемым. Можно порекомендовать адреса вида
0x80040000
0x80080000
. . .
Аргумент shmflg используется для передачи системному вызову shmat() флагов SHM_RND и SHM_RDONLY. Наличие первого из них означает, что адрес shmaddr следует округлить до некоторй системно-зависимой величины. Второй флаг предписывает присоединить сегмент только для чтения; если он не установлен, присоединенный сегмент будет доступен и на чтение, и на запись (если процесс обладает соответствующими правами).
При успешном завершении системного вызова shmdt() результат равен нулю; в случае неудачи возвращается -1.
Аргумент shmaddr задает начальный адрес отсоединяемого сегмента. После того, как последний процесс отсоединил разделяемый сегмент памяти, этот сегмент вместе с идентификатором и ассоциированной структурой данных следует удалить с помощью системного вызова shmctl.
Пример использования вызовов shmat() и shmdt() приведен ниже (рис. 23):
возможности системных вызовов shmat() и shmdt()
(операции над разделяемыми сегментами памяти) */
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
main ()
{
extern int errno;
int shmid, shmaddr, shmflg;
int flags, attach, detach, rtrn, i;
/* Цикл присоединений для данного процесса */
printf ("\nВведите число присоединений ");
printf ("для процесса (1-4): ");
scanf ("%d", &attach);
printf ("\nЧисло присоединений = %d\n", attach);
for (i = 0; i < attach; i++) {
/* Ввести идентификатор разделяемого сегмента */
printf ("\nВведите ид-р разделяемого сегмента,\n");
printf ("над которым нужно выполнить операции: ");
scanf ("%d", &shmid);
printf ("\nИд-р сегмента = %d\n", shmid);
/* Ввести адрес присоединения */
printf ("\nВведите адрес присоединения ");
printf ("в шестнадцатеричной записи: ");
scanf ("%x", &shmaddr);
printf ("\nАдрес присоединения = 0x%x\n", shmaddr);
/* Выбрать требуемые флаги */
printf ("\nВведите номер нужной комбинации флагов:\n");
printf (" SHM_RND = 1\n");
printf (" SHM_RDONLY = 2\n");
printf (" SHM_RND и SHM_RDONLY = 3\n");
printf (" Выбор = ");
scanf ("%d", &flags);
switch (flags) {
case 1:
shmflg = SHM_RND;
break;
case 2:
shmflg = SHM_RDONLY;
break;
case 3:
shmflg = SHM_RND | SHM_RDONLY;
break;
}
printf ("\nФлаги = 0%o", shmflg);
/* Выполнить системный вызов shmat */
rtrn = shmat (shmid, shmaddr, shmflg);
if (rtrn == -1) {
printf ("\nshmat завершился неудачей!\n");
printf ("\Код ошибки = %d\n", errno);
}
else {
printf ("\nshmat завершился успешно.\n");
printf ("Идентификатор shmid = %d\n", shmid);
printf ("Адрес = 0x%x\n", rtrn);
}
}
/* Цикл отсоединений для данного процесса */
printf ("\nВведите число отсоединений ");
printf ("для процесса (1-4): ");
scanf ("%d", &detach);
printf ("\nЧисло отсоединений = %d\n", detach);
for (i = 0; i < detach; i++) {
/* Ввести адрес отсоединения */
printf ("\nВведите адрес отсоединяемого сегмента ");
printf ("в шестнадцатеричной записи: ");
scanf ("%x", &shmaddr);
printf ("\nАдрес отсоединения = 0x%x\n", shmaddr);
/* Выполнить системный вызов shmdt */
rtrn = shmdt (shmaddr);
if (rtrn == -1) {
printf ("\nshmdt завершился неудачей!\n");
printf ("\Код ошибки = %d\n", errno);
}
else {
printf ("\nshmdt завершился успешно,\n");
printf ("идентификатор shmid = %d\n", shmid);
}
}
exit (0);
}
Во многих случаях при решении задач с помощью компьютера необходимо, чтобы два или более заданий исполнялись одновременно. Такой способ называется параллельным программированием. В параллельных программах постановка задачи осуществляется в виде нескольких частичных задач, которые способны выполняться параллельно. Очевидно, что программы протекают действительно ``параллельно'', если в наличии имеются как минимум 2 процессора. Однако, даже при использовании одного процессора можно получить определенные преимущества в производительности.
С помощью процессов можно организовать параллельное выполнение программ. Для этого процессы клонируются с помощью вызовов fork() или exec(), а затем между ними организуется взаимодействие средствами IPC. Это довольно дорогостоящий с точки зрения ресурсов процесс.
С другой стороны, для организации параллельного выполнения и взаимодействия можно использовать механизм многопоточности. Основной единицей здесь является поток.
Поток представляет собой облегченную версию процесса. Чтобы понять, в чем состоит его особенность, необходимо вспомнить основные характеристики процесса:
При корректной реализации потоки имеют определенные преимущества над процессами. Им требуется:
Если операционная система поддерживает концепции потоков в рамках одного процесса, она называется многопоточной. Многопоточные приложения имеют ряд преимуществ:
Существует две основных категории реализации потоков:
При использовании этого уровня ядро не знает о существовании потоков - все управление потоками реализуется приложением, с помощью специальных библиотек потоков. Переключение потоков не требует привилегий режима ядра, а планирование полностью зависит от приложения. При этом ядро управляет деятельностью процесса. Если поток вызывает системную функцию, будет блокирован весь процесс, но для поточной библиотеки этот поток будет находиться в активном состоянии. Здесь состояние потока не зависит от состояния процесса.
Преимущества пользовательских потоков:
На этом уровне все управление потоком выполняется ядром. Существует программный интерфейс приложения (системные вызовы) для работы с потоками уровня ядра. Ядро поддерживает информацию о контексте процесса и потоков, переключение между потоками требует выполнения дисциплины планирования ядра на уровне потоков.
Преимущества потоков уровня ядра:
Функция pthread_create() позволяет добавить новый поток управления к текущему процессу. Прототип функции:
void*(*start_routine)(void *), void *arg);
pthread_attr_t tattr;
pthread_t tid;
extern void *start_routine(void *arg);
void *arg;
int ret;
/* поведение по умолчанию*/
ret = pthread_create(&tid, NULL, start_routine, arg);
/* инициализация с атрибутами по умолчанию */
ret = pthread_attr_init(&tattr);
/* определение поведения по умолчанию*/
ret = pthread_create(&tid, &tattr, start_routine, arg);
Функция pthread_create() вызывается с атрибутом attr, определяющим необходимое поведение. start_routine - это функция, с которой новый поток начинает свое выполнение. Когда start_routine завершается, поток завершается со статусом выхода, установленным в значение, возвращенное start_routine.
Если вызов pthread_create() успешно завершен, идентификатор созданного потока сохраняется по адресу tid.
Создание потока с использованием аргумента атрибутов NULL оказывает тот же эффект, что и использование атрибута по умолчанию; оба создают поток по умолчанию. При инициализации tattr он получает поведение по умолчанию.
pthread_create() возвращает 0 при успешном завершении. Любое другое значение указывает, что произошла ошибка.
Функция pthread_join() используется для ожидания завершения потока:
pthread_t tid;
int ret;
int status;
/* ожидание завершения потока "tid" со статусом status */
ret = pthread_join(tid, &status);
/* ожидание завершения потока "tid" без статуса */
ret = pthread_join(tid, NULL);
Функция pthread_join() блокирует вызывающий поток, пока указанный поток не завершится. Указанный поток должен принадлежать текущему процессу и не должен быть отделен. Если status не равен NULL, он указывает на переменную, которая принимает значение статуса выхода завершенного потока при успешном завершении pthread_join(). Несколько потоков не могут ждать завершения одного и того же потока. Если они пытаются выполнить это, один поток завершается успешно, а все остальные - с ошибкой ESRCH. После завершения pthread_join(), любое пространство стека, связанное с потоком, может быть использовано приложением.
В следующем примере (рис. 26) один поток верхнего уровня вызывает процедуру, которая создает новый вспомогательный поток, выполняющий сложный поиск в базе данных, требующий определенных затрат времени. Главный поток ждет результатов поиска, и в то же время может выполнять другую работу. Он ждет своего помощника с помощью функции pthread_join(). Аргумент pbe является параметром стека для нового потока.
Исходный код для thread.c:
{
struct phonebookentry *pbe;
pthread_attr_t tattr;
pthread_t helper;
int status;
pthread_create(&helper, NULL, fetch, &pbe);
/* выполняет собственную задачу */
pthread_join(helper, &status);
/* теперь можно использовать результат */
}
void fetch(struct phonebookentry *arg)
{
struct phonebookentry *npbe;
/* ищем значение в базе данных */
npbe = search (prog_name)
if (npbe != NULL)
*arg = *npbe;
pthread_exit(0);
}
struct phonebookentry {
char name[64];
char phonenumber[32];
char flags[16];
}
Функция pthread_detach() является альтернативой pthread_join(), чтобы утилизировать область памяти для потока, который был создан с атрибутом detachstate, установленным в значение PTHREAD_CREATE_JOINABLE. Прототип функции:
pthread_t tid;
int ret;
/* отделить поток tid */
ret = pthread_detach(tid);
pthread_detach() возвращает 0 при успешном завершении. Любое другое значение указывает, что произошла ошибка.
В ОС Linux управление процессами является ключевой технологией при разработке многих программ.
Определение: Процесс - это находящаяся в состоянии выполнения программа вместе с ее средой выполнения.
Так как Linux - это настоящая многозадачная система, в ней одновременно могут выполняться несколько программ (процессов, задач). Термин ``одновременно'' не всегда соответствует действительности дословно. Обычно процессор (CPU) может работать в данный момент только с одним процессом. Если вам действительно необходимо выполнить одновременно несколько программ параллельно, нужно использовать либо несколько компьютеров, либо больше процессоров. Однако, для большинства пользователей этот вариант может быть непривлекательным из-за расходов на приобретение дополнительной техники.
Каждый процесс имеет собственное виртуальное адресное пространство. Это гарантирует, что ни один из процессов не будет подвержен помехам или влиянию со стороны других.
Отдельные процессы получают доступ к CPU по очереди. Планировщик процессов решает, как долго и в какой последовательности процессы будут занимать CPU. При этом создается впечатление, что процессы протекают действительно параллельно.
В Linux реализована вытесняющая многозадачность. Это значит, что система сама решает, как долго конкретный процесс может использовать CPU, и когда наступит очередь следующего процесса. Если вы хотите вмешаться в процесс планирования, вы можете сделать это как root с помощью команды nice.
Вы можете узнать с помощью команды ps, какие процессы выполняются в настоящий момент.
1234 pts/0 R 0:00 ps -x
TTY показывает в каком терминале выполняется процесс. Если в колонке не указано никакое значение, речь идет, как правило, о процессе - демоне.
STAT показывает текущее состояние процесса. В приведенном примере стоит значение R, означающее выполнение (running). Этот процесс выполняется именно сейчас. Для процессов применяются следующие обозначения:
COMMAND - имя команды, с помощью которой запущен процесс.
В Linux все процессы упорядочены иерархически, подобно генеалогическому дереву. Каждый процесс владеет информацией того процесса, от которого он был порожден. То же самое справедливо и для его родительского процесса и т.д.
Если вы хотите узнать, сколько времени CPU необходимо каждому процессу, Вы можете использовать команду top. Она показывает, какое время вычислений занимает определенная программа в процессоре. Обратите внимание на колонку '%CPU'.
Однопоточные программы C содержат два основных класса данных: локальные и глобальные данные. Для многопоточных программ C добавляется третий класс: данные потока. Они похожи на глобальные данные, за исключением того, что они являются собственными для потока.
Данные потока являются единственным способом определения и обращения к данным, которые принадлежат отдельному потоку. Каждый элемент данных потока связан с ключом, который является глобальным для всех потоков процесса. Используя ключ, поток может получить доступ к указателю (void *), который поддерживается только для этого потока.
Функция pthread_keycreate() используется для выделения ключа, который используется для идентифицикации данных некоторого потока в процессе. Ключ глобален для всех потоков, и все потоки в начале содержат значение ключа NULL.
pthread_keycreate() вызывается отдельно для каждого ключа перед его использованием. При этом нет никакой неявной синхронизации. Как только ключ будет создан, каждый поток может связать значение с ключом. Значения являются специфичными для потока и поддерживаются для каждого потока независимо. Связывание ключа с потоком удаляется, когда поток заканчивается, при этом ключ должен быть создан с функцией деструктора. Прототип функции:
pthread_key_t key;
int ret;
/* создание ключа без деструктора */
ret = pthread_key_create(&key, NULL);
/* создание ключа с деструктором */
ret = pthread_key_create(&key, destructor);
pthread_keycreate() возвращает 0 при успешном завершении, или любое другое значение при возникновении ошибки.
Функция pthread_keydelete() используется, чтобы уничтожить существующий ключ данных для определенного потока. Любая выделенная память, связанная с ключом, может быть освобождена, потому что ключ был удален. Ссылка на эту память возвратит ошибку.
Прототип pthread_keydelete():
pthread_key_t key;
int ret;
/* key был создан ранее */
ret = pthread_key_delete(key);
Программист должен сам нести ответственность за освобождение любых ресурсов, выделенных потоку, перед вызовом функции удаления. Эта функция не вызывает деструктор.
pthread_keydelete() возвращает 0 после успешного завершения, или любое другое значение в случае ошибки.
Функция pthread_setspecific() используется, чтобы установить связку между потоком и указанным ключом данных для потока. Прототип функции:
pthread_key_t key;
void *value;
int ret;
/* key был создан ранее */
ret = pthread_setspecific(key, value);
Чтобы получить привязку ключа для вызывающего потока, используется функция pthread_getspecific(). Полученное значение сохраняется в переменной value. Прототип функции:
pthread_key_t key;
void *value;
/* key был создан ранее */
value = pthread_getspecific(key);
...
while (write(fd, buffer, size) == -1) {
if (errno != EINTR) {
fprintf(mywindow, "%s\n", strerror(errno));
exit(1);
}
}
...
}
Ссылки на errno должны получить код системной ошибки из процедуры, вызванной этим конкретным потоком, а не некоторым другим. Поэтому ссылки на errno в одном потоке относятся к отдельной области памяти, чем ссылки на errno в других потоках. Переменная mywindow предназначена для обращения к потоку stdio, связанному с окном, которое является частным объектом потока. Также как и errno, ссылки на mywindow в одном потоке должны обращаться к отдельной области памяти (и, в конечном счете, к различным окнам). Единственное различие между этими переменными состоит в том, что библиотека потоков реализует раздельный доступ для errno, а программист должен сам реализовать это для mywindow. Следующий пример показывает, как работают ссылки на mywindow. Препроцессор преобразовывает ссылки на mywindow в вызовы процедур mywindow. Эта процедура в свою очередь вызывает pthread_getspecific(), передавая ему глобальную переменную mywindow_key (это действительно глобальная переменная) и выходной параметр win, который принимает идентификатор окна для этого потока.
Следующий фрагмент кода:
FILE *_mywindow(void) {
FILE *win;
pthread_getspecific(mywin_key, &win);
return(win);
}
#define mywindow _mywindow()
void routine_uses_win( FILE *win) {
...
}
void thread_start(...) {
...
make_mywin();
...
routine_uses_win( mywindow )
...
}
Теперь можно устанавливать собственные данные потока:
FILE **win;
static pthread_once_t mykeycreated = PTHREAD_ONCE_INIT;
pthread_once(&mykeycreated, mykeycreate);
win = malloc(sizeof(*win));
create_window(win, ...);
pthread_setspecific(mywindow_key, win);
}
void mykeycreate(void) {
pthread_keycreate(&mywindow_key, free_key);
}
void free_key(void *win) {
free(win);
}
Следующий шаг состоит в выделении памяти для элемента данных вызывающего потока. После выделения памяти выполняется вызов процедуры create_window, которая устанавливает окно для потока и выделяет память для переменной win, которая ссылается на окно. Наконец, выполняется вызов pthread_setspecific(), который связывает значение win с ключом. После этого, как только поток вызывает pthread_getspecific(), передавая глобальный ключ, он получает некоторое значение. Это значение было связано с этим ключом в вызывающем потоке, когда он вызвал pthread_setspecific(). Когда поток заканчивается, выполняются вызовы функций деструкторов, которые были настроены в pthread_key_create(). Каждая функция деструктора вызывается, если завершившийся поток установил значение для ключа вызовом pthread_setspecific().
Функция pthread_self() вызывается для получения ID вызывающего ее потока:
pthread_t tid;
tid = pthread_self();
pthread_t tid1, tid2;
int ret;
ret = pthread_equal(tid1, tid2);
Функция pthread_once() используется для вызова процедуры инициализации потока только один раз. Последующие вызовы не оказывают никакого эффекта. Пример вызова функции:
void (*init_routine)(void));
int ret;
ret = sched_yield();
Функция pthread_setschedparam() используется, чтобы изменить приоритет существующего потока. Эта функция никоим образом не влияет на дисциплину диспетчеризации:
const struct sched_param *param);
pthread_t tid;
int ret;
struct sched_param param;
int priority;
/* sched_priority указывает приоритет потока */
sched_param.sched_priority = priority;
/* единственный поддерживаемый алгоритм диспетчера*/
policy = SCHED_OTHER;
/* параметры диспетчеризации требуемого потока */
ret = pthread_setschedparam(tid, policy, ¶m);
Функция
struct schedparam *param)
Пример вызова функции:
pthread_t tid;
sched_param param;
int priority;
int policy;
int ret;
/* параметры диспетчеризации нужного потока */
ret = pthread_getschedparam (tid, &policy, ¶m);
/* sched_priority содержит приоритет потока */
priority = param.sched_priority;
Поток, как и процесс, может принимать различные сигналы:
#include <signal.h>
int sig;
pthread_t tid;
int ret;
ret = pthread_kill(tid, sig);
Если sig имеет значение 0, выполняется проверка ошибок, но сигнал реально не посылается. Таким образом можно проверить правильность tid. Функция возвращает 0 в случае успешного завершения, или другое значение в случае ошибки.
Функция pthread_sigmask() может использоваться для изменения или получения маски сигналов вызывающего потока:
#include <signal.h>
int ret;
sigset_t old, new;
ret = pthread_sigmask(SIG_SETMASK, &new, &old); /* установка новой маски */
ret = pthread_sigmask(SIG_BLOCK, &new, &old); /* блокирование маски */
ret = pthread_sigmask(SIG_UNBLOCK, &new, &old); /* снятие блокировки */
pthread_sigmask() возвращает 0 в случае успешного завершения, или другое значение в случае ошибки.
Поток может прервать свое выполнение несколькими способами:
int status;
pthread_exit(&status); /* выход возвращает статус status */
Функция pthread_cancel() предназначена для прерывания потока:
pthread_t thread;
int ret;
ret = pthread_cancel(thread);
pthread_cancel() возвращает 0 в случае успешного завершения, или другое значение в случае ошибки.
Для компиляции и сборки многопоточной программы необходимо иметь следующее:
<signal.h>, <unistd.h>
libm, libw, libintl, libnsl, libsocket, libmalloc, libmapmalloc, и др.)
Следующий список указывает некоторые из наиболее частых оплошностей и ошибок, которые могут вызвать ошибки в многопоточных программах.
Атрибуты являются способом определить поведение потока, отличное от поведения по умолчанию. При создании потока с помощью pthread_create() или при инициализации переменной синхронизации может быть определен собственный объект атрибутов. Атрибуты определяются только во время создания потока; они не могут быть изменены в процессе использования.
Таким образом, обычно вызываются три функции:
pthread_attr_t tattr;
pthread_t tid;
void *start_routine;
void arg
int ret;
/* инициализация атрибутами по умолчанию */
ret = pthread_attr_init(&tattr);
/* вызов соответствующих функций для изменения значений */
ret = pthread_attr_*(&tattr,SOME_ATRIBUTE_VALUE_PARAMETER);
/* создание потока */
ret = pthread_create(&tid, &tattr, start_routine, arg);
Объект атрибутов является закрытым и не может быть непосредственно изменен операциями присваивания. Существует множество функций, позволяющих инициализировать, конфигурировать, и уничтожать любые типы объекта. Как только атрибут инициализируется и конфигурируется, это доступен всему процессу. Поэтому рекомендуется конфигурировать все требуемые спецификации состояния один раз на ранних стадиях выполнения программы. При этом соответствующий объект атрибутов может использоваться везде, где это нужно. Использование объектов атрибутов имеет два основных преимущества:
Функция pthread_attr_init() используется, чтобы инициализировать объект атрибутов значениями по умолчанию. Память распределяется системой потоков во время выполнения.
Пример вызова функции:
pthread_attr_t tattr;
int ret;
ret = pthread_attr_init(&tattr);
Атрибут | Значение | Смысл |
Функция возвращает 0 после успешного завершения. Любое другое значение указывает, что произошла ошибка. Код ошибки устанавливается в переменной errno.
Функция pthread_attr_destroy() используется, чтобы удалить память для атрибутов, выделенную во время инициализации. Объект атрибутов становится недействительным.
Пример вызова функции:
pthread_attr_t tattr;
int ret;
ret = pthread_attr_destroy(&tattr);
Если поток создается отделенным (PTHREAD_CREATE_DETACHED), его ID потока и другие ресурсы могут многократно использоваться, как только он завершится. Если нет необходимости ожидать в вызывающем потоке завершения нового потока, можно вызвать перед его созданием функцию pthread_attr_setdetachstate().
Если поток создается неотделенным (PTHREAD_CREATE_JOINABLE), предполагается, что создающий поток будет ожидать его завершения и выполнять в созданном потоке pthread_join(). Независимо от типа потока, процесс не закончится, пока не завершатся все потоки.
pthread_attr_setdetachstate() возвращает 0 после успешного завершения или любое другое значение в случае ошибки.
Пример вызова для отсоединения потока:
pthread_attr_t tattr;
int ret;
/* устанавливаем состояние потока */
ret = pthread_attr_setdetachstate(&tattr,PTHREAD_CREATE_DETACHED);
Следующий код иллюстрирует, как можно создать отделенный поток (рис. 28):
pthread_attr_t tattr;
pthread_t tid;
void *start_routine;
void arg
int ret;
ret = pthread_attr_init(&tattr);
ret = pthread_attr_setdetachstate(&tattr,PTHREAD_CREATE_DETACHED);
ret = pthread_create(&tid, &tattr, start_routine, arg);
Функция pthread_attr_getdetachstate() позволяет определить состояние при создании потока, т.е. был ли он отделенным или присоединяемым. Она возвращает 0 после успешного завершения или любое другое значение в случае ошибки. Пример вызова:
pthread_attr_t tattr;
int detachstate;
int ret;
ret = pthread_attr_getdetachstate (&tattr, &detachstate);
Поток может быть ограничен (имеет тип PTHREAD_SCOPE_SYSTEM) или неограничен (имеет тип PTHREAD_SCOPE_PROCESS). Оба этих типа доступны только в пределах данного процесса. Функция pthread_attr_setscope() позволяет создать потоки указанных типов.
pthread_attr_setscope() возвращает 0 после успешного завершения или любое другое значение в случае ошибки. Пример вызова функции:
pthread_attr_t attr;
pthread_t tid;
void start_routine;
void arg;
int ret;
/* инициализация атрибутов по умолчанию */
ret = pthread_attr_init (&tattr);
/* ограниченное поведение */
ret = pthread_attr_setscope(&tattr, PTHREAD_SCOPE_SYSTEM);
ret = pthread_create (&tid, &tattr, start_routine, arg);
pthread_attr_t tattr;
int scope;
int ret;
ret = pthread_attr_getscope(&tattr, &scope);
Стандарт POSIX определяет несколько значений атрибута планирования:
SCHED_FIFO, SCHED_RR (Round Robin), или SCHED_OTHER
(метод приложения). ДисциплиныSCHED_FIFO и SCHED_RR
являются необязательными, и поддерживаются только для потоков в режиме
реального времени.
Библиотека pthreads поддерживает только значение SCHED_OTHER. Попытка установить другое значение приведет к возникновению ошибки ENOSUP.
Для установки дисциплины диспетчеризации используется следующая функция:
pthread_attr_t tattr;
int ret;
ret = pthread_attr_setschedpolicy(&tattr, SCHED_OTHER);
Функция pthread_attr_setinheritsched() используется для наследования дисциплины диспетчеризации из родительского потока. Значение переменной inherit, равное PTHREAD_INHERIT_SCHED (по умолчанию) означает, что будет использована дисциплина планирования, определенная в создающем потоке, а любые атрибуты планирования, определенные в вызове pthread_create() будут проигнорированы. Если используется константа PTHREAD_EXPLICIT_SCHED, используются атрибуты, переданные в ввызове pthread_create().
Функция возвращает 0 при успешном завершении, и любое другое значение в случае ошибки. Пример вызова этой функции:
pthread_attr_t tattr;
int ret;
ret = pthread_attr_setinheritsched(&tattr, PTHREAD_EXPLICIT_SCHED);
Параметры диспетчеризации определены в структуре sched_param; в настоящее время поддерживается только приоритет sched_param.sched_priority. Этот приоритет задается целым числом, при этом чем выше значение, тем выше приоритет потока при планировании. Создаваемые потоки получают этот приоритет.
Функция pthread_attr_setschedparam() используется, чтобы установить значения в этой структуре. При успешном завершении она возвращает 0. Пример использования:
pthread_attr_t tattr;
int newprio;
sched_param param;
/* устанавливает приоритет */
newprio = 30;
param.sched_priority = newprio;
/* устанавливает параметры диспетчеризации */
ret = pthread_attr_setschedparam (&tattr, ¶m);
const struct sched_param *param) используется для получения приоритета текущего потока.
Как правило, стеки потоков начинаются на границах страниц, и любой указанный размер округляется к следующей границе страницы. К вершине стека добавляется страница без разрешения на доступ, чтобы переполнение стека вызвало посылку сигнала SIGSEGV потоку, вызвавшему переполнение.
Если определяется стек, то поток должен создаваться с типом
PTHREAD_CREATE_JOINABLE. Этот стек не может быть освобожден,
пока не произойдет выход из pthread_join() этого потока,
потому что стек потока не может быть освобожден, пока поток не закончится.
Единственный надежный способ закончить такой поток - вызов pthread_join().
В общем случае нет необходимости выделять пространство для стека потоков. Библиотека потоков выделяет один мегабайт виртуальной памяти для стека каждого потока без резервирования пространства выгрузки. (Библиотека использует опцию MAP_NORESERVE для mmap, чтобы выделить память).
Каждый стек потоков, созданный библиотекой потоков, имеет красную зону. Библиотека создает красную зону, добавляя к вершине стека страницу, чтобы обнаружить переполнение стека. Эта страница недействительна и вызывает ошибку защиты памяти, когда к ней обращаются. Красные зоны добавляются ко всем автоматически распределенным стекам, в независимости от того, был ли определен размер стека приложением, или используется размер по умолчанию.
Обычно создание собственного стека предполагает стек, который немного отличается от стека по умолчанию. Как правило, задача состоит в выделении более чем одного мегабайта для стека. Иногда стек по умолчанию, наоборот, является слишком большим. Можно создать тысячи потоков, и тогда виртуальной памяти будет недостаточно, чтобы работать с гигабайтами пространств стека при использовании размера по умолчанию.
Абсолютный минимальный предел размера стека можно определить, вызывая макрос PTHREAD_STACK_MIN (определенный в <pthread.h>), который возвращает количество памяти стека, требуемого для потока, выполняющего пустую процедуру (NULL). Реальные потоки нуждаются в большем стеке, поэтому нужно очень осторожно сокращать размер стека.
Функция pthread_attr_setstacksize() используется для установки размера стека текущего потока.
Атрибут stacksize определяет размер стека в байтах. Этот стек выделяется системой и его размер не должен быть меньше минимального размера стека. При успешном завершении функция возвращает 0. Пример вызова:
pthread_attr_t tattr;
int stacksize;
int ret;
/* установка нового размера */
stacksize = (PTHREAD_STACK_MIN + 0x4000);
ret = pthread_attr_setstacksize(&tattr, stacksize);
Функция pthread_attr_getstacksize(pthread_attr_t *tattr, size_t *size) используется для получения размера стека текущего потока:
pthread_attr_t tattr;
int stacksize;
int ret;
/* получение размера стека */
ret = pthread_attr_getstacksize(&tattr, &stacksize);
Иногда возникает потребность установить базовый адрес стека. Для этого используется функция pthread_attr_setstackaddr():
Пример показывает создание потока со стеком определенного размера по указанному адресу:
pthread_attr_t tattr;
pthread_t tid;
int ret;
void *stackbase;
int size = PTHREAD_STACK_MIN + 0x4000;
stackbase = (void *) malloc(size);
/* инициализация значениями по умолчанию */
ret = pthread_attr_init(&tattr);
/* установка размера стека */
ret = pthread_attr_setstacksize(&tattr, size);
/* установка базового адреса стека */
ret = pthread_attr_setstackaddr(&tattr, stackbase);
ret = pthread_create(&tid, &tattr, func, arg);
Системный планировщик использует таблицу процессов, описанную в заголовочном файле
При выполнении нескольких потоков они будут неизменно взаимодействовать друг с другом, чтобы синхронизировать свое выполнение. Существует несколько методов синхронизации потоков:
Объекты синхронизации можно разместить в файлах, где они будут существовать независимо от создавшего их процесса.
Основные ситуации, которые требуют использования синхронизации:
Блоки взаимного исключения - общий метод сериализации выполнения потоков. Мьютексы синхронизируют потоки, гарантируя что только один поток в некоторый момент времени выполняет критическую секцию кода. Мьютексы можно использовать и в однопоточном коде.
Атрибуты мьютекса могут быть связаны с каждым потоком. Чтобы изменить атрибуты мьютекса по умолчанию, можно объявить и инициализировать объект атрибутов мьютекса, а затем изменить определенные значения. Часто атрибуты мьютекса устанавливаются в одном месте в начале приложения, чтобы быстро найти и изменить их.
После того, как сформированы атрибуты мьютекса, можно непосредственно инициализировать мьютекс. Доступны следующие действия с мьютексом: инициализация, удаление, захват или открытие, попытка захвата.
Функция pthread_mutexattr_init() используется, чтобы инициализировать
атрибуты, связанные с объектом, значениями по умолчанию. Память для
каждого объекта атрибутов выделяется системой поддержки потоков во
врем выполнения. mattr - закрытый тип, который содержит системный
объект атрибутов. Возможные значения типа mattr - PTHREAD_PROCESS_PRIVATE
(по умолчанию) и PTHREAD_PROCESS_SHARED. При вызове этой
функции значение по умолчанию атрибута pshared равно
PTHREAD_PROCESS_PRIVATE, что означает, что инициализированный
мьютекс может использоваться в пределах процесса.
Прежде, чем повторно инициализировать объект атрибутов мьютекса, он должен сначала быть удален функцией pthread_mutexattr_destroy(). Вызов функции pthread_mutexattr_init() возвращает указатель на закрытый объект. Если объект не удалить, может произойти утечка памяти. pthread_mutexattr_init() возвращает 0 после успешного завершения, или другое значение, если произошла ошибка.
Пример вызова функции:
pthread_mutexattr_t mattr;
int ret;
/* инициализация атрибутов значениями по умолчанию */
ret = pthread_mutexattr_init(&mattr);
pthread_mutexattr_t mattr;
int ret;
/* удаление атрибутов */
ret = pthread_mutexattr_destroy(&mattr);
Область видимости мьютекса может быть либо некоторый процесс, либо вся система. Функцияpthread_mutexattr_setpshared() используется, чтобы установить область видимости атрибутов мьютекса.
Если мьютекс был создан с атрибутом pshared, установленным
в состояние
PTHREAD_PROCESS_SHARED, и он существует
в разделяемой памяти, то он может быть разделен среди потоков нескольких
процессов. Если атрибут pshared у мьютекса установлен в PTHREAD_PROCESS_PRIVATE,
то оперировать этим мьютексом могут только потоки, созданные тем же
самым процессом.
pthread_mutexattr_setpshared() возвращает 0 после успешного завершения, или другое значение, если произошла ошибка. Пример вызова:
pthread_mutexattr_t mattr;
int ret;
ret = pthread_mutexattr_init(&mattr);
/* переустановка на значение по умолчанию: private */
ret = pthread_mutexattr_setpshared(&mattr, PTHREAD_PROCESS_PRIVATE);
int *pshared) используется для получения области видимости текущего мьютекса потока:
pthread_mutexattr_t mattr;
int pshared, ret;
/* получить атрибут pshared для мьютекса */
ret = pthread_mutexattr_getpshared(&mattr, &pshared);
Функция pthread_mutex_init() предназначена для инициализации мьютекса:
const pthread_mutexattr_t *mattr);
Захват через мьютекс не должен повторно инициализироваться или удаляться, пока другие потоки могут его использовать. Если мьютекс инициализируется повторно или удаляется, приложение должно убедиться, что в настоящее время этот мьютекс не используется. pthread_mutex_init() возвращает 0 после успешного завершения, или другое значение, если произошла ошибка. Пример вызова:
pthread_mutex_t mp = PTHREAD_MUTEX_INITIALIZER;
pthread_mutexattr_t mattr;
int ret;
/* инициализация мьютекса значением по умолчанию */
ret = pthread_mutex_init(&mp, NULL);
ret = pthread_mutexattr_init(&mattr);
/* смена значений mattr с помощью функций */
ret = pthread_mutexattr_*();
/* инициализация мьютекса произвольными значениями */
ret = pthread_mutex_init(&mp, &mattr);
Функция pthread_mute_lock() используется для запирания мьютекса. Если мьютекс уже закрыт, вызывающий поток блокируется и мьютекс ставится в очередь приоритетов. Когда происходит возврат из pthread_mute_lock(), мьютекс запирается, а вызывающий поток становится его владельцем. pthread_mute_lock() возвращает 0 после успешного завершения, или другое значение, если произошла ошибка. Пример вызова:
pthread_mutex_t mp;
int ret;
ret = pthread_mutex_lock(&mp);
Мьютекс должен быть закрыт, а вызывающий поток должен быть владельцем, то есть тем, кто запирал мьютекс. Пока любые другие потоки ждут доступа к мьютексу, поток в начале очереди не блокирован. pthread_mutex_unlock() возвращает 0 после успешного завершения, или другое значение, если произошла ошибка. Пример вызова:
pthread_mutex_t mp;
int ret;
ret = pthread_mutex_unlock(&mp);
Функция pthread_mutex_trylock() пытается провести запирание мьютекса. Она является неблокирующей версией pthread_mutex_lock(). Если мьютекс уже закрыт, вызов возвращает ошибку. В противном случае, мьютекс закрывается, а вызывающий процесс становится его владельцем. pthread_mutex_trylock() возвращает 0 после успешного завершения, или другое значение, если произошла ошибка. Пример вызова:
pthread_mutex_t mp;
int ret; ret = pthread_ mutex_trylock(&mp);
Функция pthread_mutex_destroy() используется для удаления мьютекса в любом состоянии. Память для мьютекса не освобождается. pthread_mutex_destroy() возвращает 0 после успешного завершения, или другое значение, если произошла ошибка. Пример вызова:
pthread_mutex_t mp;
int ret;
ret = pthread_mutex_destroy(&mp);
Функция increment_count() использует мьютекс, чтобы гарантировать атомарность модификации разделяемой переменной count.
Функция get_count() использует мьютекс, чтобы гарантировать, что переменная count атомарно считывается (рис. 29):
pthread_mutex_t count_mutex;
long long count;
void increment_count() {
pthread_mutex_lock(&count_mutex);
count = count + 1;
pthread_mutex_unlock(&count_mutex);
}
long long get_count() {
long long c;
pthread_mutex_lock(&count_mutex);
c = count;
pthread_mutex_unlock(&count_mutex);
return (c);
}
Иногда может возникнуть необходимость доступа к нескольким ресурсам сразу. При этом возникает проблема, когда два потока пытаются захватить оба ресурса, но запирают соответствующие мьютексы в различном порядке.
В этом примере, если два потока запирают мьютексы 1 и 2, то возникает тупик при попытке запереть другой мьютекс.
/* использует ресурс 1 */ /* использует ресурс 2 */
pthread_mutex_lock(&m1); pthread_mutex_lock(&m2);
/* теперь захватывает ресурсы 2 /* теперь захватывает ресурсы 1
+ 1 */ + 2 */
pthread_mutex_lock(&m2); pthread_mutex_lock(&m1);
Если блокировка всегда выполняется в указанном порядке, тупик не возникнет. Однако, эта техника может использоваться не всегда:
Иногда требуется запирать мьютексы в другом порядке, чем предписанный.
Чтобы предотвратить тупик в этой ситуации, лучше использовать функцию
pthread_mutex_trylock(). Один из потоков должен освободить
свой мьютекс, если он обнаруживает, что может возникнуть тупик.
Ниже проиллюстрирован подход условной блокировки:
Поток 1:
pthread_mutex_lock(&m2);
/* нет обработки */
pthread_mutex_unlock(&m2);
pthread_mutex_unlock(&m1);
pthread_mutex_lock(&m2);
if(pthread_mutex_trylock(&m1)==0)
/* захват! */
break;
/* уже заперт */
pthread_mutex_unlock(&m2);
}
/* нет обработки */
pthread_mutex_unlock(&m1);
pthread_mutex_unlock(&m2);
Для порождения процессов в ОС Linux существует два способа. С одной стороны, процесс может полностью заменить другой процесс, без замены среды выполнения. С другой стороны, можно создать новый процесс с помощью системного вызова fork(). Синтаксис вызова следующий:
#include <unistd.h>
pid_t fork(void);
Процесс-потомок и процесс-родитель получают разные коды возврата после вызова fork(). Процесс-родитель получает идентификатор (PID) потомка. Если это значение будет отрицательным, то при создании процессов произошла ошибка. Процесс-потомок получает в качестве кода возврата значение 0, если вызов fork() произошел успешно.
Таким образом, можно проверить, был ли создан новый процесс.
{
case -1: /*при вызове fork() возникла ошибка*/
case 0 : /*это код потомка*/
default : /*это код родительского процесса*/
}
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
main()
{
pid_t pid;
int rv;
switch(pid=fork()) {
case -1:
perror("fork"); /* произошла ошибка */
exit(1); /* выход из родительского процесса */
case 0:
printf(" CHILD: Это процесс-потомок!\n");
printf(" CHILD: Мой PID - %d\n", getpid());
printf(" CHILD: PID моего родителя - %d\n",getppid());
printf(" CHILD: Введите мой код возврата (как можно меньше):");
scanf(" %d");
printf(" CHILD: Выход!\n");
exit(rv);
default:
printf("PARENT: Это процесс-родитель!\n");
printf("PARENT: Мой PID - %d\n", getpid());
printf("PARENT: PID моего потомка %d\n", pid);
printf("PARENT: Я жду, пока потомок не вызовет exit()...\n");
wait();
printf("PARENT: Код возврата потомка:%d\n",WEXITSTATUS(rv));
printf("PARENT: Выход!\n");
}
}
Когда потомок вызывает exit(), код возврата передается родителю, который ждет его, вызвав wait(). WEXITSTATUS() представляет собой макрос, который получает фактический код возврата потомка из вызова wait().
Функция wait() ждет завершения первого из всех возможных потомков родительского процесса. Иногда необходимо точно определить, какой из потомков должен завершиться. Для этого используется вызов waitpid() с соответствующим PID потомка в качестве аргумента. Еще один момент, на который следует обратить внимание, это то, что и родитель и потомок используют переменную rv. Это не означает, что переменная разделена между процессами. Каждый процесс содержит собственные копии всех переменных.
Рассмотрим следующий пример (рис. 2).
#include <stdio.h>
#include <unistd.h>
int main()
{
char pid[255];
fork();
fork();
fork();
sprintf(pid, "PID : %d\n",getpid());
write(STDOUT_FILENO, pid, strlen(pid));
exit(0);
}
В этом случае будет создано семь процессов-потомков. Первый вызов fork() создает первого потомка. Как указано выше, процесс наследует положение указателя команд от родительского процесса. Указатель команд содержит адрес следующего оператора программы. Это значит, что после первого вызова fork(), указатель команд и родителя, и потомка находится перед вторым вызовом fork().После второго вызова fork() и родитель, и первый потомок производят потомков второго поколения - всего процессов становится 4. После третьего вызова fork() каждый процесс производит своего потомка, увеличивая общее число процессов до 8.
Так называемые процессы - зомби возникают, если потомок завершился, а родительский процесс не вызвал wait(). Для завершения процессы используют либо оператор возврата, либо вызов функции exit() со значением, которое будет возвращено операционной системе. Операционная система оставляет процесс зарегистрированным в своей внутренней таблице данных, пока родительский процесс не получит кода возврата потомка, либо не закончится сам. В случае процесса-зомби его код возврата не передается родителю и запись об этом процессе не удаляется из таблицы процессов операционной системы. При дальнейшей работе и появлении новых зомби таблица процессов может быть заполнена, что приведет к невозможности создания новых процессов.
Вложение элементов запирания мьютекса в связанную структуру данных и простые изменения в коде связного списка позволяют предотвратить тупик, осуществляя блокировку в предписанном порядке.
Структура для блокировки имеет вид:
int value;
struct node1 *link;
pthread_mutex_t lock;
} node1_t;
Чтобы удалить узел из списка, необходимо:
node1_t *prev,
*current; prev = &ListHead;
pthread_mutex_lock(&prev->lock);
while ((current = prev->link) != NULL) {
pthread_mutex_lock(¤t->lock);
if (current->value == value) {
prev->link = current->link;
pthread_mutex_unlock(¤t->lock);
pthread_mutex_unlock(&prev->lock);
current->link = NULL;
return(current);
}
pthread_mutex_unlock(&prev->lock);
prev = current;
}
pthread_mutex_unlock(&prev->lock);
return(NULL);
}
Переменные состояния используются, чтобы атомарно блокировать потоки, пока не наступит специфическое состояние. Переменные состояния всегда используются в сочетании с блокировками мьютексов:
Функция pthread_condattr_init() инициализирует атрибуты,
связанные с объектом значениями по умолчанию. Память для каждого объекта
атрибутов cattr, выделяется системой потоков в процессе выполнения.
cattr является закрытым типом данных, который содержит созданный
системой объект атрибутов. Возможные значения признаков видимости
cattr - PTHREAD_PROCESS_PRIVATE и
PTHREAD_PROCESS_SHARED.
Значение по умолчанию атрибута pshared при вызове этой функции
- PTHREAD_PROCESS_PRIVATE, что означает, что инициализированная
переменная состояния может использоваться в пределах процесса.
Прежде, чем атрибут переменной состояния сможет использоваться повторно, он должен повторно инициализироваться функцией pthread_condattr_destroy(). Вызов pthread_condattr_init()возвращает указатель на закрытый объект. Если объект не будет удален, возникнет утечка памяти.
pthread_condattr_init() возвращает 0 после успешного завершения. Любое другое значение указывает, что произошла ошибка. Пример вызова функции:
pthread_condattr_t cattr;
int ret;
ret = pthread_condattr_init(&cattr);
Областью видимости переменной состояния может быть либо процесс, либо вся система, как и для мьютексов. Если переменная состояния создана с атрибутом pshared, установленным в состояние PTHREAD_PROCESS_SHARED, и она существует в разделяемой памяти, то эта переменная может разделяться среди потоков нескольких процессов. Если же атрибут pshared установлен в PTHREAD_PROCESS_PRIVATE (по умолчанию), то лишь потоки, созданные тем же самым процессом, могут оперировать переменной.
Функция pthread_condattr_setpshared() используется, чтобы установить область видимости переменной состояния. Она возвращает 0 после успешного завершения. Любое другое значение указывает, что произошла ошибка. Пример использования функции:
pthread_condattr_t cattr;
int ret;
/* Область видимости - все процессы */
ret = pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_SHARED);
/* Внутренняя переменная для процесса */
ret = pthread_condattr_setpshared(&cattr, PTHREAD_PROCESS_PRIVATE);
int *pshared) используется для получения области видимости переменной состояния.
Функция pthread_cond_init() инициализирует переменную состояния:
const pthread_condattr_t *cattr);
Статические переменные состояния могут инициализироваться непосредственно значениями по умолчанию с помощью макроса PTHREAD_COND_INITIALIZER. Несколько потоков не должны одновременно инициализировать или повторно инициализировать ту же самую переменную состояния. Если переменная состояния повторно инициализируется или удаляется, приложение должно убедиться, что эта переменная состояния больше не используется.
pthread_cond_init() возвращает 0 после успешного завершения. Любое другое значение указывает, что произошла ошибка. Пример использования функции:
pthread_cond_t cv;
pthread_condattr_t cattr;
int ret;
/* инициализация значениями по умолчанию */
ret = pthread_cond_init(&cv, NULL);
/* инициализация определенными значениями */
ret = pthread_cond_init(&cv, &cattr);
Функция pthread_cond_wait() используется, чтобы атомарно освободить мьютекс и заставить вызывающий поток блокироваться по переменной состояния. Функция pthread_cond_wait() возвращает 0 после успешного завершения. Любое другое значение указывает, что произошла ошибка. Пример использования функции:
pthread_cond_t cv;
pthread_mutex_t mutex;
int ret;
ret = pthread_cond_wait(&cv, &mutex);
Проверка состояния обычно проводится в цикле while, который
вызывает
pthread_cond_wait():
while(condition_is_false)
pthread_cond_wait();
pthread_mutex_unlock();
Следует всегда вызывать pthread_cond_signal() под защитой мьютекса, используемого с сигнальной переменной состояния. В ином случае, переменная состояния может измениться между тестированием соответствующего состояния и блокировкой в pthread_cond_wait(), что может вызвать бесконечное ожидание. Если никакие потоки не блокированы по переменной состояния, вызов pthread_cond_signal () не имеет никакого эффекта.
Следующий фрагмент кода иллюстрирует, как избежать бесконечного ожидания, описанного выше:
pthread_cond_t count_nonzero;
unsigned count;
decrement_count() {
pthread_mutex_lock(&count_lock);
while (count == 0)
pthread_cond_wait(&count_nonzero, &count_lock);
count = count - 1;
pthread_mutex_unlock(&count_lock);
}
increment_count() {
pthread_mutex_lock(&count_lock);
if (count == 0)
pthread_cond_signal(&count_nonzero);
count = count + 1;
pthread_mutex_unlock(&count_lock);
}
pthread_mutex_t *mp, const struct timespec *abstime);
#include <time.h>
pthread_timestruc_t to;
pthread_cond_t cv;
pthread_mutex_t mp;
timestruct_t abstime;
int ret;
/* ожидание переменной состояния */
ret = pthread_cond_timedwait(&cv, &mp, &abstime);
pthread_mutex_lock(&m);
to.tv_sec = time(NULL) + TIMEOUT;
to.tv_nsec = 0;
while (cond == FALSE) {
err = pthread_cond_timedwait(&c, &m, &to);
if (err == ETIMEDOUT) {
/* таймаут */
break;
}
}
pthread_mutex_unlock(&m);
pthread_cond_broadcast() возвращает 0 после успешного завершения, или любое другое значение в случае ошибки.
Поскольку pthread_cond_broadcast() заставляет все потоки, блокированные некоторым состоянием, бороться за мьютекс, ее нужно использовать аккуратно. Например, можно использовать pthread_cond_broadcast(), чтобы позволить потокам бороться за изменение количества требуемых ресурсов, когда ресурсы освобождаются:
pthread_mutex_t rsrc_lock;
pthread_cond_t rsrc_add;
unsigned int resources;
get_resources(int amount) {
pthread_mutex_lock(&rsrc_lock);
while (resources < amount)
pthread_cond_wait(&rsrc_add, &rsrc_lock);
resources -= amount;
pthread_mutex_unlock(&rsrc_lock);
}
add_resources(int amount) {
pthread_mutex_lock(&rsrc_lock);
resources += amount;
pthread_cond_broadcast(&rsrc_add);
pthread_mutex_unlock(&rsrc_lock);
}
pthread_cond_t cv;
int ret;
/* Condition variable is destroyed */
ret = pthread_cond_destroy(&cv);
Сокеты обеспечивают двухстороннюю связь типа ``точка-точка'' между двумя процессами. Они являются основными компонентами межсистемной и межпроцессной связи. Каждый сокет представляет собой конечную точку связи, с которой может быть связано некоторое имя. Он также имеет определенный тип, и один или нескольких связанных с ним процессов.
Сокеты существуют в областях связи (доменах). Домен сокета - это абстракция, которая определяет структуру адресации и набор протоколов. Сокеты могут соединяться только с сокетами в том же самом домене. Всего определены 23 класса сокетов (см. <sys/socket.h>), из которых обычно используются только UNIX-сокеты и Интернет-сокеты. Сокеты могут использоваться для установки связи между процессами на отдельной системе, подобно другим формам IPC.
Класс сокетов UNIX обеспечивает адресное пространство сокетов для отдельной вычислительной системы. Сокеты области UNIX называются именами файлов UNIX. Сокеты также можно использовать, чтобы организовать связь между процессами на различных системах. Адресное пространство сокетов между связанными системами называют доменом Интернета. Коммуникации домена Интернета используют стек протоколов TCP/IP.
Типы сокетов определяют особенности связи, доступные приложению. Процессы взаимодействуют только через сокеты одного и того же типа. Существует следующие основные типы сокетов:
Сервер | Клиент
Установка сокета socket() |
Для создания сокета определенного типа в определенном адресном пространстве используется функция socket():
#include <sys/ socket. h>
int socket(int domain, int type, int protocol);
Тем не менее, удаленный процесс не может идентифицировать определенный сокет, пока ему не будет присвоен адрес. Процессы могут поддерживать связь только через адреса. В пространстве адресов UNIX соединение обычно определяется одним или двумя именами файлов. В пространстве адресов Интернета соединение определяется локальным и удаленным адресами и номерами портов.
Функция bind()
#include <sys/ socket. h>
int bind(int s, const struct sockaddr *name, int namelen);
ushort_ t sa_ family; /* семейство адресов */
char sa_ data[ 14]; /* 14 байт прямого адреса */
};
ushort_ t sun_ family; /* AF_ UNIX */
char sun_ path[ 104]; /* путь к файлу */
};
uchar_ t sin_ len;
sa_ family_ t sin_ family; /* AF_ INET */
in_ port_ t sin_ port; /* 16-битный порт */
struct in_ addr sin_ addr; /* Указатель на адрес */
uchar_ t sin_ zero[ 8]; /* зарезервировано */
};
Функция exec() (execute) загружает и запускает другую программу. Таким образом, новая программа полностью замещает текущий процесс. Новая программа начинает свое выполнение с функции main. Все файлы, открытые вызывающей программой, остаются открытыми. Они также являются доступными новой программе. Существуют следующие 6 различных вариантов функций exec.
int execl(char *name, char *arg0, ... /*NULL*/);
int execv(char *name, char *argv[]);
int execle(char *name, char *arg0, ... /*,NULL, char *envp[]*/);
int execve(char *name, char *arv[], char *envp[]);
int execlp(char *name, char *arg0, ... /*NULL*/);
int execvp(char *name, char *argv[]);
Буквы l, v, p и e в конце имен функций определяют формат и объем аргументов, а также каталоги, в которых нужно искать загружаемую программу.
int main(int argc, char *argv[])
{
int i=0;
printf("%s\n",argv[0]);
printf("Программа запущена и получила строку : ");
while(argv[++i] != NULL)
printf("%s ",argv[i]);
return 0;
}
Эта программа выводит на экран строку, переданную ей в качестве аргумента. Пусть она называется hello. Она будет вызвана из другой программы с помощью функции execl(). Код вызывающей программы ниже (рис. 4.):
#include <unistd.h>
int main(int argc, int *argv[])
{
printf("Будет выполнена программа %s...\n\n", argv[0]);
printf("Выполняется %s", argv[0]);
execl("hello","","Hello", "World!", NULL);
return 0;
}
В строке execl() указаны аргументы в виде списка. Доступ к ним также осуществляется последовательно. Если использовать функцию execv(), то вместо списка будет указан вектор аргументов (рис. 5):
#include <unistd.h>
int main(int argc, int *argv[])
{
printf("Программа %s будет выполнена...\n\n", argv[0]);
printf("Выполняется %s", argv[0]);
execv("hello",argv);
return 0;
}
Соединение сокетов обычно происходит несимметрично. Один из процессов действует как сервер, а другой процесс выполняет роль клиента. Сервер связывает свой сокет с предварительно указанным путем или адресом. После этого для сокетов вида SOCK_STREAM сервер вызывает функцию listen(), которая определяет, сколько запросов на соединение можно поставить в очередь. Клиент запрашивает соединение с сокетом сервера вызовом connect(), а сокет принимает некоторое соединение с помощью функции accept(). Синтаксис вызова listen() следующий:
#include <sys/ socket. h>
int listen (int socket, int backlog );
Функция accept() используется сервером для принятия соединения с сокетом. При этом сокет в момент вызова функции должен уже иметь очередь запросов, созданную вызовом listen(). Если сервер устанавливает связь с клиентом, то функция accept() возвращает новый сокет-дескриптор, через который и происходит общение клиента с сервером. Пока устанавливается связь клиента с сервером, функция accept() блокирует другие запросы связи с данным сервером, а после установления связи "прослушивание" запросов возобновляется.
#include <sys/ socket. h>
int accept( int socket, struct sockaddr *addr, int *addrlen );
Функция connect() используется процессом-клиентом для установления связи с сервером.
#include <sys/ socket. h>
int connect( int socket, struct sockaddr *name, int namelength );
Для обмена данными существуют две группы функции - для записи в сокет и для чтения из него. Функции для записи имеют вид:
#include <sys/ socket. h>
#include <sys/ uio. h>
int send( int socket, const char *msg, int len, int flags);
int sendto( int socket, const char *msg, int len, int flags,
const struct sockaddr *to, int tolen );
int sendmsg( int socket, const struct msghdr *msg, int flags );
Для приема данных процесс-потребитель должен выполнить функцию приема или чтения данных из сокета. Варианты функций приема:
#include <sys/ socket. h>
#include <sys/ uio. h>
int recv( int socket, char *buffer, int len, int flags);
int recvfrom( int socket, char *buffer, int len, int flags,
const struct sockaddr *from, int fromlen );
int recvmsg( int socket, const struct msghdr *msg, int flags );
Функция shutdown() используется для немедленного закрытия всех или части связей для некоторого сокета.
#include <sys/ uio. h>
int shutdown(int s, int how);
#include <sys/ uio. h>
int close (int s);
Пример-оболочка программы "Клиент" (рис. 32)
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#define ADDRESS "mysocket" /* адрес для связи */
void main ()
{
char c;
int i, s, len;
FILE *fp;
struct sockaddr_un sa;
/* получаем свой сокет-дескриптор: */
if ((s = socket (AF_UNIX, SOCK_STREAM, 0))<0) {
perror ("client: socket"); exit(1);
}
/* создаем адрес, по которому будем связываться с сервером: */
sa.sun_family = AF_UNIX;
strcpy (sa.sun_path, ADDRESS);
/* пытаемся связаться с сервером: */
len = sizeof ( sa.sun_family) + strlen ( sa.sun_path);
if ( connect ( s, &sa, len) < 0 ){
perror ("client: connect"); exit (1);
}
/* читаем сообщения сервера */
fp = fdopen (s, "r");
c = fgetc (fp);
/* обрабатываем информацию от сервера
...................................
*/
/* посылаем ответ серверу */
send (s, "client", 7, 0);
/* продолжаем диалог с сервером, пока в этом есть необходимость
............................
*/
/* завершаем сеанс работы */
close (s);
exit (0);
}
Пример-оболочка программы "Сервер" (рис. 33)
#include <sys/socket.h>
#include <sys/un.h>
#include <stdio.h>
#define ADDRESS "mysocket" /* адрес для связи */
void main ()
{
char c;
int i, d, d1, len, ca_len;
FILE *fp;
struct sockaddr_un sa, ca;
/* получаем свой сокет-дескриптор: */
if((d = socket (AF_UNIX, SOCK_STREAM, 0)) < 0) {
perror ("client: socket"); exit (1);
}
/* создаем адрес, c которым будут связываться клиенты */
sa.sun_family = AF_UNIX;
strcpy (sa.sun_path, ADDRESS);
/* связываем адрес с сокетом; уничтожаем файл с именем ADDRESS,
если он существует, для того, чтобы вызов bind завершился успешно */
unlink (ADDRESS);
len = sizeof ( sa.sun_family) + strlen (sa.sun_path);
if ( bind ( d, &sa, len) < 0 ) {
perror ("server: bind"); exit (1);
}
/* слушаем запросы на сокет */
if ( listen ( d, 5) < 0 ) {
perror ("server: listen"); exit (1);
}
/* связываемся с клиентом через неименованный сокет
с дескриптором d1:*/
if (( d1 = accept ( d, &ca, &ca_len)) < 0 ) {
perror ("server: accept"); exit (1);
}
/* пишем клиенту: */
send (d1, "server", 7, 0);
/* читаем запрос клиента */
fp = fdopen (d1, "r");
c = fgetc (fp);
/* ................................ */
/* обрабатываем запрос клиента, посылаем ответ и т.д.
........................... */
/* завершаем сеанс работы */
close (d1);
exit (0);
}
Рассмотренные методы синхронизации процессов и коммуникаций предполагали использование одного компьютера. Однако, часто приложения должны работать в пределах локальной или распределенной сети. Одним из методов реализации взаимодействия является удаленный вызов процедур remote procedure calls (RPC). Вызов процедуры представляет собой классическую форму синхронной коммуникации: вызывающий процесс передает управление подпроцессу и ждет возвращения результатов. Используя RPC, программисты распределенных приложений могут не учитывать деталей при обеспечении интерфейса с сетью. Транспортная независимость RPC изолирует приложение от физических и логических элементов механизма коммуникаций данных и позволяет ему использовать разнообразие транспортных протоколов.
RPC делает модель вычислений клиент/сервер более мощной и более простой для программирования. Использование компиляторов протоколов ONC RPCGEN позволяет клиентам прозрачное осуществление удаленных вызовов через локальный интерфейс процедуры.
Как и при обычном вызове функции, при вызове RPC аргументы вызова передаются удаленной процедуре, и вызывающий процесс ждет ответа, который будет возвращен из удаленной процедуры. Порядок действий следующий:
Клиент осуществляет вызов процедуры, которая посылает запрос серверу и ждет. Поток выполнения блокируется, пока не будет получен ответ, или не наступит тайм-аут. Когда приходит запрос, сервер вызывает процедуру диспетчеризации, которая выполняет требуемое действие и посылает ответ клиенту. После того, как вызов RPC закончен, программа клиента продолжает выполнение.
Удаленная процедура уникально идентифицируется тройкой: (номер программы, номер версии, номер процедуры). Номер программы идентифицирует группу соотносящихся удаленных процедур, каждая из которых имеет уникальный номер процедуры. Программа может состоять из одной или более версий. Каждая версия состоит из множества процедур, которые могут быть вызваны удаленно. Номера версии позволяют использоваться одновременно нескольким версиям RPC протокола. Каждая версия содержит множество процедур, которые можно вызвать удаленно. Каждая процедура имеет свой номер процедуры.
Для разработки приложения RPC необходимо выполнить следующие шаги:
Самый простой способ определения и реализации протокола состоит в том, чтобы использовать компилятор протоколов типа rpcgen. Для создания протокола нужно идентифицировать имена сервисных процедур и типы данных возвращаемых аргументов и параметров. Компилятор протокола читает определения и автоматически создает коды для сервера и клиента. rpcgen использует собственный язык (язык RPC или RPCL), который очень похож на язык директив препроцессора. rpcgen реализован в виде автономного компилятора, который работает со специальными файлами, обозначенными расширением .x.
Для обработки файла RPCL необходимо выполнить:
rpcprog_clnt.c - процедуры клиента
rpcprog_svc.c - процедуры сервера
rpcprog_xdr.c - фильтры XDR
rpcprog.h - файл заголовка, необходимый для XDR фильтров.
Внешнее представление данных (XDR - eXternal Data Representation) - абстракция данных, необходимая для машинно-независимой связи. Клиент и сервер могут быть машинами различных типов.
Пусть программа клиента называется rpcprog.c, а программа
сервера - rpcsvc.c. Протокол был определен в файле rpcprog.x.
Этот файл был обработан rpcgen, чтобы создать файлы фильтров
и процедур: rpcprog_clnt.c, rpcprog_svc.c,
rpcprog_xdr.c,
rpcprog.h.
Программы клиента и сервера должны включать строку #include "rpcprog.h"
После этого необходимо:
Откомпилировать код клиента:
Здесь перечислены все процедуры RPC для всех уровней протокола удаленного вызова.
Упрощенный интерфейс - это самый простой уровень использования RPC, потому что он не требует использования других процедур RPC. Он также ограничивает контроль над основными механизмами коммуникации. Разработка программ для этого уровня может осуществляться быстро и непосредственно поддерживается компилятором rpcgen. Для большинства приложений достаточно возможностей rpcgen. Некоторые службы RPC не доступны в виде функций C, но они доступны как программы RPC. Процедуры библиотеки упрощенного интерфейса обеспечивают прямой доступ к возможностям RPC для программ, которые не требуют детального управления.
Все процедуры находятся в библиотеке служб RPC librpcsvc.
Пример rusers.c, приведенный ниже (рис. 34), показывает число пользователей на удаленном компьютере. Он вызывает процедуру библиотеки RPC rusers.
#include <rpcsvc/rusers.h>
#include <stdio.h>
/*
* программа вызывает службу rusers()
*/
main(int argc,char **argv)
{
int num;
if (argc != 2) {
fprintf(stderr, "Использование: %s hostname\n",
argv[0]);
exit(1);
}
if ((num = rnusers(argv[1])) < 0) {
fprintf(stderr, "Ошибка вызова: rusers\n");
exit(1);
}
fprintf(stderr, "%d пользователей на %s\n", num,
argv[1] );
exit(0);
}
Клиентская часть (рис. 35) состоит из вызова функции rpc_call(). Синтаксис функции приведен ниже:
u_long prognum /* Номер программы сервера */,
u_long versnum /* Номер версии сервера */,
xdrproc_t inproc /* фильтр XDR для кодирования arg */,
char *in /* Указатель на аргументы */,
xdr_proc_t outproc /* Фильтр декодирования результата */,
char *out /* Адрес сохранения результата */,
char *nettype /* Выбор транспортной службы */);
Клиент блокируется вызовом rpc_call() до тех пор, пока он не получит ответ от сервера. Если сервер отвечает, то возвращается RPC_SUCCESS со значением 0. Если запрос был неудачен, возвращается значение, отличное от 0. Это значение можно преобразовать к типу clnt_stat, перечислимому типу, определенному в файле RPC (<rpc/rpc.h>) и интерпретируемому функцией clnt_sperrno(). Эта функция возвращает указатель на стандартное сообщение RPC об ошибке, соответствующее коду ошибки. В примере испытываются все "видимые" транспортные службы, внесенные в /etc/netconfig. Настройка количества повторов требует использования более низких уровней библиотеки RPC. Множественные аргументы и результаты обрабатываются с помощью объединения их в структуры.
#include <utmp.h>
#include <rpc/rpc.h>
#include <rpcsvc/rusers.h>
/* программа вызывает удаленную программу RUSERSPROG */
main(int argc, char **argv)
{
unsigned long nusers;
enum clnt_stat cs;
if (argc != 2) {
fprintf(stderr, "Использование: rusers hostname\n");
exit(1);
}
if( cs = rpc_call(argv[1], RUSERSPROG,
RUSERSVERS, RUSERSPROC_NUM, xdr_void,
(char *)0, xdr_u_long, (char *)&nusers,
"visible") != RPC_SUCCESS ) {
clnt_perrno(cs);
exit(1);
}
fprintf(stderr, "%d пользователей на компьютере %s\n", nusers,
argv[1] );
exit(0);
}
Так как типы данных могут быть представлены различным образом на различных машинах, rpc_call() нужно указать и тип аргумента, и указатель на него (аналогично и для результата). Возвращаемое значение для RUSERSPROC_NUM - unsigned long, поэтому первым возвращаемым параметром rpc_call() будет xdr_u_long, а вторым - *nusers. Поскольку RUSERSPROC_NUM не имеет аргументов, функцией шифрования XDR для rpc_call() будет xdr_void(), а ее аргумент имеет значение NULL.
Программа сервера, использующая упрощенный интерфейс, достаточно простая (рис. 36). Она вызывает rpc_reg(), чтобы зарегистрировать процедуру, которая будет вызвана, а затем вызывает svc_run(), диспетчера удаленных процедур библиотеки RPC, который ждет входящих запросов.
Прототип rpc_reg():
u_long versnum /* Номер версии сервера */,
u_long procnum /* Номер процедуры сервера */,
char *procname /* Имя удаленной функции */,
xdrproc_t inproc /* Фильтр для кодирования аргумента arg */,
xdrproc_t outproc /* Фильтр декодирования результата*/,
char *nettype /* Выбор транспортной службы */);
Некоторые замечания относительно программы сервера:
#include <rpc/rpc.h>
#include <rpcsvc/rusers.h>
void *rusers();
main()
{
if(rpc_reg(RUSERSPROG, RUSERSVERS,
RUSERSPROC_NUM, rusers,
xdr_void, xdr_u_long,
"visible") == -1) {
fprintf(stderr, "Невозможно зарегистрировать\n");
exit(1);
}
svc_run(); /* Процедура без возврата */
fprintf(stderr, "Ошибка: Выход из svc_run!\n");
exit(1);
}
rpc_reg() можно вызвать сколько угодно раз, чтобы зарегистрировать все различные программы, версии, и процедуры.
Типы данных, передаваемые и получаемые от удаленных процедур, могут быть любыми из множества предопределенных типов, либо типом, определенным программистом. RPC работает с произвольными структурами данных, независимо от различий в структуре типов на различных машинах, преобразовывая типы к стандартному формату передачи, который называется внешним представлением данных (XDR). Преобразование из машинного представления в XDR называют сериализацией, а обратный процесс - десериализацией. Аргументы транслятора для rpc_call() и rpc_reg() могут определять примитивную процедуру XDR, например xdr_u_long(), или специальную процедуру программиста, которая обрабатывает полную структуру аргументов. Процедуры обработки аргументов должны принимать только два аргумента: указатель на результат и указатель на обработчик XDR.
Доступны следующие примитивные процедуры XDR для обработки типов данных:
xdr_long() xdr_float() xdr_u_int() xdr_bool()
xdr_short() xdr_double() xdr_u_short() xdr_wrapstring()
xdr_char() xdr_quadruple() xdr_u_char() xdr_void()
В случае собственной процедуры программиста, структура
int a;
short b;
} simple;
#include "simple.h"
bool_t xdr_simple(XDR *xdrsp, struct simple *simplep)
{
if (!xdr_int(xdrsp, &simplep->a))
return (FALSE);
if (!xdr_short(xdrsp, &simplep->b))
return (FALSE);
return (TRUE);
}
Процедура XDR возвращает результат, отличный от нуля, если она завершается успешно, либо 0 в случае ошибки.
Для более сложных структур данных используют готовые процедуры XDR.
xdr_vector() xdr_union() xdr_pointer()
xdr_string() xdr_opaque()
int *data;
int arrlnth;
} arr;
{
return(xdr_array(xdrsp, (caddr_t)&arrp->data,
(u_int *)&arrp->arrlnth, MAXLEN, sizeof(int), xdr_int));
}
bool_t xdr_intarr(XDR *xdrsp, int intarr[])
{
return (xdr_vector(xdrsp, intarr, SIZE, sizeof(int), xdr_int));
}
Строки, законченные пустым указателем, транслируются с помощью xdr_string(). Она сходна с xdr_bytes(), но без параметра длины. При сериализации процедура получает длину строки из strlen(), а при десериализации создает законченную пустым указателем строку.
xdr_reference() вызывает встроенные функции xdr_string() и xdr_reference(), которые преобразуют указатели, для передачи строки, и struct simple из предыдущего примера. Пример использования xdr_reference() (рис. 37):
char *string;
struct simple *simplep;
} finalexample;
bool_t xdr_finalexample(XDR *xdrsp, struct finalexample *finalp)
{
if (!xdr_string(xdrsp, &finalp->string, MAXSTRLEN))
return (FALSE);
if (!xdr_reference( xdrsp, &finalp->simplep, sizeof(struct simple),
xdr_simple)) return (FALSE);
return (TRUE);
}
Процедура thatxdr_simple(), должна вызываться вместо xdr_reference().
В качестве примера высокоуровневого приложения приведен удаленный аналог команды чтения оглавления каталога.
Вначале рассматривается локальная версия. Программа состоит из двух файлов:
lls.c - основная программа, которая вызывает процедуру в локальном модуле read_dir.c
#include <strings.h>
#include "rls.h"
main (int argc, char **argv)
{
char dir[DIR_SIZE];
/* вызов локальной процедуры */
strcpy(dir, argv[1]);/* char dir[DIR_SIZE] это имя каталога */
read_dir(dir);
/* вывод результата */
printf("%s\n", dir);
exit(0);
}
и возвращают один результат. Оба передаются через указатели.
Возвращаемые значения должны указывать на статические данные. */
#include <stdio.h>
#include <sys/types.h>
#include <sys/dir.h>
#include "rls.h"
read_dir(char *dir) /* char dir[DIR_SIZE] */
{
DIR * dirp;
struct direct *d;
printf("начало");
/* открывает каталог */
dirp = opendir(dir);
if (dirp == NULL)
return(NULL);
/* сохраняет имена файлов в буфер каталога */
dir[0] = NULL;
while (d = readdir(dirp))
sprintf(dir, "%s%s\n", dir, d->d_name);
/* выводит результат */
printf("выход ");
closedir(dirp);
return((int)dir);
}
Для того, чтобы модифицировать программу для работы через сеть, выполняются следующие действия:
Для передачи и приема имени каталога и его содержимого можно использовать простые строки, заканчивающиеся пустым указателем. Кроме того, передача этих параметров включена непосредственно в код сервера и клиента.
После этого нужно определить номера программы, процедуры и версии для клиента и сервера. Это можно сделать автоматически, используя rpcgen, или на базе предопределенных макросов упрощенного интерфейса. В примере номера определены вручную.
Сервер и клиент должны заранее согласовать, что они будут использовать логические адреса (физические адреса не имеют значение, поскольку они скрыты от разработчика приложения).
Номера программы определяются стандартным способом:
0x20000000 - 0x3FFFFFFF: Пользовательские
0x40000000 - 0x5FFFFFFF: Переходные
0x60000000 - 0xFFFFFFFF: Резервированные
DIR_SIZE определяет размер буфера для каталога в программах сервера и клиента.
Теперь файл rls.h содержит:
#define DIRPROG ((u_long) 0x20000001) /* номер программы сервера */
#define DIRVERS ((u_long) 1) /* номер версии */
#define READDIR ((u_long) 1) /* номер процедуры */
Для передачи данных в виде строк нужно определить процедуру XDR - фильтра xdr_dir(), который разделяет данные. При этом можно обрабатывать только один аргумент шифрования и расшифровки. Для этого подходит стандартная процедура xdr_string().
Файл XDR, rls_xrd.c, выглядит так:
#include "rls.h"
bool_t xdr_dir(XDR *xdrs, char *objp)
{ return ( xdr_string(xdrs, &objp, DIR_SIZE) ); }
Для нее можно использовать оригинальный файл read_dir.c. Необходимо лишь зарегистрировать процедуру и запустить сервер.
Процедура регистрируется с помощью функции registerrpc():
u_long versnum /* Номер версии сервера */,
u_long procnum /* Номер процедуры сервера */,
char *procname /* Имя удаленной функции */,
xdrproc_t inproc Фильтр для кодирования аргументов */,
xdrproc_t outproc /* Фильтр декодирования результата */);
#include "rls.h"
main()
{
extern bool_t xdr_dir();
extern char * read_dir();
registerrpc(DIRPROG, DIRVERS, READDIR,
read_dir, xdr_dir, xdr_dir);
svc_run();
}
На клиентской стороне просто производится вызов удаленной процедуры. Для этого используется функция callrpc():
u_long prognum /* Номер программы сервера */,
u_long versnum /* Номер версии сервера */,
char *in /* Указатель на аргументы */,
xdrproc_t inproc /* Фильтр XDR для кодирования аргумента */,
char *out /* Адрес для сохранения результата */
xdr_proc_t outproc /* Фильтр декодирования результата */);
Программа rls.c выглядит так:
* rls.c: клиент удаленного чтения каталога
*/
#include <stdio.h>
#include <strings.h>
#include <rpc/rpc.h>
#include "rls.h"
main (argc, argv)
int argc; char *argv[];
{
char dir[DIR_SIZE];
/* вызов удаленной процедуры */
strcpy(dir, argv[2]);
read_dir(argv[1], dir); /* read_dir(host, directory) */
/* вывод результата */
printf("%s\n", dir);
exit(0);
}
read_dir(host, dir)
char *dir, *host;
{
extern bool_t xdr_dir();
enum clnt_stat clnt_stat;
clnt_stat = callrpc ( host, DIRPROG, DIRVERS, READDIR,
xdr_dir, dir, xdr_dir, dir);
if (clnt_stat != 0) clnt_perrno (clnt_stat);
}
Программа rpcgen создает модули интерфейса удаленной программы. Она компилирует исходный код, написанный на языке RPC. Язык RPC подобен по синтаксису и структуре на C. rpcgen создает один или несколько исходных модулей на языке C, которые затем обрабатываются компилятором C.
Результатом работы rpcgen являются: