11

Comunicação entre processos

(IPC – InterProcess Communication)

 

 

Notas:

 

1.     É condição prévia à aprendizagem deste assunto (IPC), o domínio sobre as funções read e write de I/O de baixo-nível.

2.     O livro [2] de W. Stevens, Unix networking programming, é a principal referência sobre este assunto.

3.     O livro [1] de W. Stevens, Advanced programming in the Unix environment, serve só de apoio à aprendizagem.

4.     Deve ser criada a biblioteca libnet.a (referente ao código do livro [2] da bibliografia) antes de testar quaisquer exemplos e exercícios. Em alternativa poderá remover as referencias nos programas as funções de error (err_sys etc ) e substituir por mensagens mais simples, imprimindo para o stderr ou substituir por macros da sua escolha.

5.     Os ficheiros dos livros [1] e[2] podem ser encontrados em formato compactado, tar.gz e descompactados, num directório tipo  $CURSO/stevens

6.     Em alternativa poderá fazer linkagem ao ficheiro libnet.a previamente criado. O path do ficheiro será indicado pelo docente.

 

 

Acesso aos livros de Stevens pode ser feito através da página do Stevens http://www.kohala.com/start/
No capítulo sobre Processos vimos as principais primitivas de controlo de processos, em particular a primitiva fork() que permite gerar processos. Mas, a única maneira destes processos comunicarem entre si era através da passagem de ficheiros abertos aquando dum fork() ou dum exec(), ou ainda através do próprio sistema de ficheiros.

 

Neste capítulo, outras técnicas de comunicação entre processos (IPC ou “interprocess communication”) serão descritas, nomeadamente:

 

§  Pipetas (pipes)

§  FIFOs

§  Filas de mensagens

§  Memória partilhada

§  Semáforos

§  Sockets (máteria adicional)

 

 

Pipetas

 

Uma pipeta estabelece um canal IPC unidireccional entre dois programas de tal modo que a saída dum deles será a entrada do outro. Nenhum ficheiro intermédio é utilizado.

 

Na linha de comando do Unix, uma pipeta é criada com o comando ‘|’.

 

Exemplos 11.1, 11.2 e 11.3:

1.  who | sort

2.  ls | wc –l

3.  who | sort | grep 2005 | wc –l

 

NOTA:

Embora o resultado das seguintes sequências de comandos :

1. com1 | com2

2. com1 > temp ; com2 < temp ; rm temp

seja idêntico, existe uma diferença fundamental  entre elas.

 

A diferença reside não tanto na utilização do ficheiro intermédio temp, mas mais na sincronização dos comandos com1 e com2. No segundo caso, com2 só começa a ser executado depois de com1 ter terminado, ao passo que no primeiro caso os dois comandos são executados duma forma concorrente.

 

De seguida, vamos ver como abrir uma pipeta (|) dentro dum programa C, abrindo assim um canal de comunicação entre dois processos

 

Há dois tipos de pipetas:  (1) pipetas não-formatadas (2) pipetas formatadas

 

Também existem dois tipos de pipetas dependendo como os processos que vão utilizar as pipetas foram criados. Sem-Nome no caso erem criados via fork() e Com-Nome quando são criados independentemente.


Pipetas não-formatadas (ou de baixo-nível)

 

Uma pipeta não-formatada não tem nome. Ao contrário dos ficheiros normais, uma pipeta sem nome tem dois (2) descritores de ficheiros associados, e não 1 descritor de ficheiro. Uma pipeta não-formatada é criada com a seguinte função ou chamada ao sistema:

 

#include <unistd.h>

 

int pipe( int filedes[2] );

 

Retorna: 0 se OK, -1 em caso de erro.  Argumento de retorno: 2 descritores de ficheiros no array filedes, com filedes[0] aberto para leitura e filedes[1] aberto para escrita.

 

Exemplo 11.4:

(see p.102 de [2] da bibliografia; compilação c/ libnet.a)

Se não vai usar a biblioteca do libnet.a : #define err_sys(s) fprintf(stderr,s)

 

O seguinte programa ilustra a criação duma pipeta através da chamada ao sistema pipe. Esta função devolve dois ficheiros (abertos) sem nome, identificados pelos descritores (inteiros) armazenados no array pipefd.  Note-se que este é um caso particular de comunicação dum processo com ele próprio (Figura 11.1).

 

#include <unistd.h>

#include <stdio.h>

main()

{

            int             pipefd[2], n;

            char            buff[100];

 

            if (pipe(pipefd) < 0)   err_sys("pipe error");

 

            printf("read fd = %d, write fd = %d ", pipefd[0], pipefd[1]);

            if (write(pipefd[1], "hello world\n", 12) != 12)

                      err_sys("write error");

 

            if ( (n = read(pipefd[0], buff, sizeof(buff))) <= 0 )

                      err_sys("read error");

 

            write(1, buff, n); /* fd 1 = stdout*/

}

 

A saída deste programa é o seguinte:

 

hello world                       

read fd = 3,  write fd = 4

     

Note-se que a cadeia de caracteres “hello world” pode ser impressa na saída estandardizada (ecrã) antes da saída da instrução printf. Isto acontece porque a saída de printf é buffered pela biblioteca estandardizada de I/O, e portanto a sua saída não chega para preencher o buffer I/O (que é tipicamente 512 ou 1024 bytes). Isto quer dizer que o buffer não é descarregado até o processo terminar. Contudo, a função write não é buffered, e portanto a saída é imediata.

 

O diagrama duma pipeta associada a um único processo é representado na Figura 11.1.

 

 

 

 

 

 

 

 

 

 

 

 


Figura 11.1: Pipeta associada a um só processo.

 

Mas, as pipetas são de pouca utilidade quando associadas a um único processo. De facto, as pipetas são normalmente usadas para estabelecer a comunicação entre dois processos como se segue:

1.     Um processo (progenitor) cria uma pipeta;

2.     O progenitor faz a chamada ao sistema fork para criar uma cópia dele próprio, i.e. o processo progénito (Figura 11.2);

3.     O progenitor fecha a extremidade de leitura (read) da pipeta e o progénito fecha a extremidade de escrita (write) da pipeta.

 

Deste modo, estabelece-se um canal IPC unidireccional de dados entre os dois processos (Figura 11.3). Note-se que o que acontece após o fork depende do sentido do fluxo de dados que se pretende. Se o sentido fosse do progénito para o progenitor, o progénito fechava a extremidade de leitura e o progenitor fechava a extremidade de escrita da pipeta.

 

 

 

 

 

 

 

 

 

 

 


Figura 11.2: Pipeta entre processos, imediatamente após fork.

 

 

 

 

 

 

 

 

 

 

 

 


Figura 11.3: Comunicação uni-direccional via pipeta do progenitor para o progénito.

Quando uma extremidade duma pipeta é fechada, as seguintes regras são válidas:

1.     Se se ler (read) duma pipeta cuja extremidade de escrita foi fechada, após todos os dados terem sido lidos, read devolve 0 para indicar o fim do ficheiro.

2.     Se se escrever (write) para uma pipeta cuja extremidade de leitura foi fechada, o sinal SIGPIPE é gerado.

 

Exemplo 11.5:

Envio de dados do progenitor para o progénito através duma pipeta (Figura 11.3).

(see p.431 de [1] da bibliografia)

Se não vai usar a biblioteca do libnet.a : #define err_sys(s) {fprintf(stderr,s); return 0;}

 

 

#include <unistd.h>

#include <stdio.h>

#define MAXLINE 4096

 

int main(void)

{

            int        n, fd[2];

            pid_t     pid;

            char      line[MAXLINE];

 

            if ( pipe(fd) < 0 )  err_sys("pipe error");

 

            if ( (pid = fork() ) < 0)  err_sys("fork error");

 

            if (pid > 0) {                                         /* progenitor - parent */

                        close(fd[0]);                                   /* desnecessario */

                        write(fd[1], "Hello world\n", 12);            /* escrever para o progénito */

             }

             else  {                                                  /* progénito – child*/

                        close(fd[1]);                                  /* desnecessario */

                        n = read(fd[0], line, MAXLINE);             /* ler do progenitor */

                        write(STDOUT_FILENO, line, n);

            }

}

 


 

Quando um utilizador digita o seguinte comando

 

who | sort | lpr

 

na linha de comando do Unix, acontece que são criados três processos e duas pipetas entre eles:

§  who é um programa que faz a saída dos login names, terminal names e login times de todos os utilizadores activos no sistema;

§  sort é um programa que ordena a lista anterior pelos login names;

§  lpr é um programa que envia a lista ordenada anterior para a impressora.

Este pipeline está representado na Figura 11.4.

 

 

 

 

 

 

 

 

 

 

 


Figura 11.4: Duas pipetas entre três processos.

 

Exercício :

 

Usando o exemplo 11.5 como base elabora um programa (mypipe.c) que utilize uma pipeta e o processo fork() para  fazer o simulacro do comando do bashls | sort

 

O processo progenitor executar o programa “sort” usando a chamada execvp() e o processo progénito deverá executar o comando “ls”.

 

Nota que terá de fechar o STDIN/STDOUT_FILENO do processo após o fork() e fazer a duplicação (dup) do descritor do ficheiro do pipeta de leitura/escrita e depois fechar os descritores do pipeta antes de chamar a função execvp()

 

·       Create Pipe

·       Fork

·       if (child) close stdout file descriptor, dup , close pipe file descriptors, execvp ls

·       else  close stdin file descriptor, dup , close pipe file descriptors, execvp sort

 

 


 

Pipetas Bidireccionais

 

Note-se que todas as pipetas criadas até agora são todas unidireccionais. Quando se pretende um canal bidireccional entre dois processos temos que criar duas pipetas, uma para cada sentido. Isto requer os seguintes passos (Figura 11.5):

 

§  Criar pipeta 1 e pipeta 2;

§  Bifurcação de processos (fork);

§  Progenitor fecha a extremidade de leitura da pipeta 1;

§  Progenitor fecha a extremidade de escrita da pipeta 2;

§  Progénito fecha a extremidade de escrita da pipeta 1;

§  Progénito fecha a extremidade de leitura da pipeta 2.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


Figura 11.5: Duas pipetas constituintes dum canal bidireccional.

 

 


 

Exemplo 11.6: (Cliente-Servidor)

(see p.431 de [1] da bibliografia; compilação c/ $ make s_pipe  na directoria ~/stevens.net/ipc)

 

Em alternativa pode obter código aqui

svn --username aluno checkout https://spocs.it.ubi.pt:8443/svn/crocker/sisops/trunk/ 

cd trunk ; cd ipc

 

Pretende-se estabelecer a comunicação bidireccional entre um cliente e um servidor através de pipetas (Figura 11.6). O cliente lê o nome dum ficheiro a partir da entrada estandardizada, e de seguida envia-o para o canal IPC. Por sua vez, o servidor lê o tal nome do ficheiro a partir do canal IPC, e tenta depois abrir o ficheiro no modo de leitura. Se o servidor consegue abrir o ficheiro, o ficheiro é lido e depois escrito para o canal IPC; caso contrário, o servidor responde com uma mensagem de erro. Então, o cliente lê a partir do canal IPC, escrevendo para a saída estandardizada o que recebe do canal IPC. Se o ficheiro não pode ser aberto pelo servidor, o cliente lê uma mensagem de erro do canal IPC; caso contrário, o cliente lê o conteúdo do ficheiro.

 

 

 

 

 

 

 


Figura 11.6: Exemplo cliente-servidor.

 

A função main cria a pipeta e o processo progénito através de fork. O cliente corre no progenitor e o servidor é executado no progénito.

 

A compilação do programa está descrita no ficheiro Makefile da directoria $CURSO/stevens/unp/ipc como se segue:

 

s_pipe: mainpipefork.c sub_clifd.c sub_servfd.c $(MYLIB)

        cc $(CFLAGS) -o mainpipe mainpipefork.c sub_clifd.c sub_servfd.c $(MYLIB)

 

onde $(MYLIB) é a biblioteca libnet.a.

Como se pode ver, este programa envolve três ficheiros:

·       mainpipefork.c         - que contém o programa principal;

·       sub_clifd.c                    - que contém a função client;

·       sub_servfd.c                - que contém a função server.

 

Alternativamente para quem não quer usar a biblioteca do Stevens

 

·       mainpipefork.c         - que contém o programa principal;

·       sub_clifd.c                    - que contém a função client;

·       sub_servfd.c                - que contém a função server.

·       error.c             -que contem funções auxiliares e de error

·       cs.h    -que contenha os ficheiros de inclusão, protótipos e constantes

·       Makefile                             -para gerir o Projecto
O programa principal é o seguinte:

mainpipefork.c

 
 


#include "cs.h"

 

main()

{

            int childpid, pipe1[2], pipe2[2];

 

            if (pipe(pipe1) < 0 || pipe(pipe2) < 0)

                        err_sys("Can’t create pipes! ");

 

            if ( (childpid = fork()) < 0 )

                        err_sys("can’t fork! ");

           

            if ( childpid > 0 )                                   /* parent */

            {

                        close(pipe1[0]);

                        close(pipe2[1]);

 

                        client(pipe2[0],pipe1[1]);

 

                        while (wait((int *) 0) != childpid)           /* wait for child to finish*/

                                    ;

 

                        close(pipe1[1]);

                        close(pipe2[0]);

                        exit(0);

            }

            else                                          /* child */

            {

                        close(pipe1[1]);

                        close(pipe2[0]);

 

                        server(pipe1[0],pipe2[1]);

 

                        close(pipe1[0]);

                        close(pipe2[1]);

                        exit(0);

                                   

            }

}

 


 A função server encontra-se no seguinte ficheiro:

sub_servfd.c

 
#include "cs.h"

 

int server(int readfd, int writefd)

{

            char      buff[MAXBUFF];

            char      errmesg[256];

            int        n, fd;

            extern int errno;

 

            /* Read the filename from the IPC descriptor */

            if ( (n = read(readfd, buff, MAXBUFF)) != MAXBUFF )

                        err_sys("Server: filename read error!");

 

            if ( (fd = open(buff, 0)) <  0 )

            {

                        /* Error. Format an error message and send it back to the client. */

                        sprintf(errmesg, ": can’t open, %s\n", sys_err_str());

                        strcat(buff, errmesg);

                        n = strlen(buff);

                        if (write(writefd, buff, n) != n)

                                    err_sys("Server: errmesg write error! ");

            }

            else

            {

                        /* Read the data from the file and write to the IPC descriptor. */

                        while ( (n = read(fd, buff, MAXBUFF)) > 0 )

                                    if (write(writefd, buff, n) != n)

                                                err_sys("Server: data write error! ");

                        if (n < 0)

                                    err_sys("Server: read error! ");

            }

}


 

A função client encontra-se no seguinte ficheiro:

sub_clifd.c

 
 


#include "cs.h"

 

int client(int readfd, int writefd)

{

            char      buff[MAXBUFF];

            int        n;

 

            /* Read the filename from the standard input, write it to the IPC descriptor */

            if (fgets(buff, MAXBUFF, stdin) == NULL)

                        err_sys("Client: filename read error!");

 

            n = strlen(buff);

            if (buff[n-1] == '\n')

                        buff[n-1]='\0';                                      /* ignore newline from fgets() */

            if (write(writefd, buff, MAXBUFF) != MAXBUFF)

                        err_sys("Client: filename write error!");

 

            /* Read the data from the IPC descriptor and write to standard output */

            while ( (n = read(readfd, buff, MAXBUFF)) > 0 )

                        if (write(1, buff, n) != n)                        /* fd 1 = stdout */

                                    err_sys("Client: data write error!");

            if (n < 0)

                        err_sys("Client: data read error!");

}

 

 

Makefile

 
 


CC = cc

OBJS = error.o mainpipefork.o sub_clifd.o sub_servfd.o

fs_pipe : $(OBJS) cs.h

            $(CC) -o s_pipe $(OBJS)

clean :

            rm fs_pipe $(OBJS)

 

error.c

 
As funções de erro, constantes e protótipos encontram-se nos seguintes ficheiros:

 

#include "cs.h"

extern int errno;

//return string of last system call error code

char * sys_err_str( void ) {

   return strerror(errno);

}

 

 

 

 

 

 

 

cs.h

 
 


#include <unistd.h>

#include <stdio.h>

#include <stdlib.h>

#include <sys/types.h>

#include <sys/stat.h>

#include <fcntl.h>

#include <errno.h>

#include <unistd.h>

#include <sys/wait.h>

#include <string.h>

 

#define err_sys(STR)    fprintf(stderr,STR)

#define err_sys2(STR,V) fprintf(stderr,STR,V)

 

#define MAXBUFF 1024

 

char *sys_err_str(void);

 

int client  (int readPipe , int  writePipe);

int server (int readPipe , int  writePipe);

 

 

Exercícios:

 

1) Insere um “prompt” no cliente para pedir o nome do ficheiro

 

2) No servidor não permite que sejam pedidos ficheiros  cujo nomes contêm caminhos relativos (baste utilizar a função strstr e pesquisa se o nome do ficheiro contenha o string “..” ) nem ficheiros que começam com as caracteres ´/’ ou ‘\’

 

3)  (Difícil) Escreva um cliente e  servidor de ficheiros onde ficheiros podem ser pedidos e servidos até que no cliente seja introduzido a palavra “sair”

 

No protocolo original quando o server envie “zero bytes” e quando o cliente recebe “zero bytes”  temos o fim do processo. Portanto - Sugestão mudar o protocolo de transmissão dos dados de ficheiro

 

Novo Protocolo:

 

C-S -> Nome

S-C -> Pacote

          Pacote = Um Block de 4bytes(tamanho do Payload) +payload (BUFFSIZE)

          Assim - Tamanho do payload =0 à fim do processo de transmissão.

 

Exemplo para o ficheiro “test.txt” que tem 8 bytes “123345678”

 

C-S à  test.txt

S-C à  Pacote = 12 bytes  um inteiro de 4 bytes = 8 + “12345678”

S-C à  Pacote = 4 bytes    um inteiro de 4 bytes = 0

 

Nota que o cliente em vez de fazer um read terá que fazer dois !!

 

Pipetas formatadas (alto-nível)

 

A biblioteca estandardizada de I/O (stdio.h) fornece uma função, designada por popen, que permite criar uma pipeta e um novo processo que ou lê ou escreve da/para a pipeta.

 

#include <unistd.h>

 

FILE *popen(char *command, const char *type);

 

Retorna: file pointer se OK, NULL em caso de erro.

int pclose(FILE *fp);

Retorna: estado de terminação de command, ou -1 em caso de erro.

 

 

A função popen abre uma pipeta para I/O, em que command é o processo que será ligado ao processo invocador. Esta função faz um fork e um exec para executar command, e devolve um ponteiro para um ficheiro I/O estandardizado. Se type é “r”, o ponteiro é ligado à saída estandardizada de command (Figura 11.7). Se type é “w”, o ponteiro é ligado à entrada estandardizada de command (Figura 6.8).

A função pclose fecha o stream I/O estandardizado e espera pela terminação do comando.

 

 

 

 

 

 

 

 


Figura 11.7: Resultado de fp = popen(command, “r”).

 

 

 

 

 

 

 

 


Figura 11.8: Resultado de fp = popen(command, “w”).

 


Exemplo 11.7: (Cliente-Servidor)

As funções popen e pclose, conjuntamente com o comando cat,  podem ser usadas para implementar o nosso exemplo anterior de comunicação bidireccional entre um processo cliente e um processo servidor.

 

mainpopen.c

 
 


#include <stdio.h>

 

#define MAXLINE1024

 

main()

{

            int        n;

            char      line[MAXLINE], command[MAXLINE + 10];

            FILE      *fp;

 

            /* Read the filename from standard input*/

            if (fgets(line, MAXLINE, stdin) == NULL)

                        err_sys("filename read error! ");

           

            /* Use popen to create a pipe and execute the command. */

            sprintf(command, "cat %s", line);

            if ( (fp = popen(command, “r”)) == NULL )

                        err_sys("popen error");

            /* Read the data from the FILE pointer and write to standard output. */

            while ( (fgets(line, MAXLINE, fp)) != NULL )

            {

                        n = strlen(line);

                        if (write(1,line,n) != n)

                                    err_sys("data write error! ");

            }

 

            if ( ferror(fp) )

                        err_sys("fgets error! ");

 

            pclose(fp);

            exit(0);

}

 

 

A compilação do programa está descrita no Makefile da directoria $CURSO/stevens/unp/ipc como se segue:

 

mainpopen:      mainpopen.c $(MYLIB)

        cc $(CFLAGS) -o mainpopen mainpopen.c $(MYLIB)

 

onde $(MYLIB) é a biblioteca libnet.a.

Como se pode ver, este programa envolve um só ficheiro:

·       mainpopen.c            - que contém o programa principal.

 

 

 


Exemplo 11.8:

Escreva um programa que obtenha a directoria corrente (através do comando pwd) e que, depois, a escreva no ecrã.

getpwd.c

 
 


#include <stdio.h>

 

#define             MAXLINE           255

 

main()

{

            FILE      *fp;

            char      line[MAXLINE];

 

            if ( (fp = popen("/bin/pwd", "r")) == NULL )

                        err_sys("popen error! ");

 

            if (fgets(line, MAXLINE, fp) == NULL)

                        err_sys("fgets error! ");

 

            printf("%s", line);          /* pwd inserts the newline*/

 

            pclose(fp);

            exit(0);

}

 

 

A compilação do programa está descrita no ficheiro Makefile da directoria $CURSO/stevens/unp/ipc como se segue:

 

getpwd:     getpwd.o $(MYLIB)

        cc $(CFLAGS) -o getpwd getpwd.o $(MYLIB)

 

onde $(MYLIB) é a biblioteca libnet.a.

Como se pode ver, este programa envolve um só ficheiro:

·       getpwd.c                  - que contém o programa principal;

 

 

A grande desvantagem das pipetas é que só podem ser usadas entre processos que têm um processo progenitor em comum. Isto acontece porque uma pipeta é passada dum processo para outro através da chamada ao sistema fork, e também porque todos os ficheiros abertos são partilhados entre o processo progenitor e o processo progénito após o fork. Não há qualquer possibilidade de criar uma pipeta entre dois processos independentes ou não-relacionados.

 

 


FIFOs (pipetas c/ nome)

 

As FIFOs são também designadas por pipetas com nome. O fluxo dos dados também é unidireccional, mas as FIFOs têm um identificador alfanumérico ou nome.

 

Para criar/utilzar named pipes no bash sehll ver:  http://www.linuxjournal.com/article/2156

 

Para utilizar programaticamente vamos ver o named pipe API.

 

#include <sys/types.h>

#include <sys/stat.h>

 

int mknod(char *pathname, int mode, int dev);

int mkfifo(char *pathname, mode_t mode);

 

Retorna: 0 se OK, -1 em caso de erro.

 

As duas funções anteriores podem ser usadas indistintamente para criar uma FIFO, pois a função mkfifo chama sempre a função mknod. A função mknod é mais de baixo-nível, e serve para criar não só FIFOs, mas também para criar e montar dispositivos no sistema. Isto significa que o valor da variável dev é ignorado no caso da criação duma FIFO.

A variável pathname indica um caminho da árvore de ficheiros que não é mais do que o nome da FIFO. O argumento mode especifica o acesso à FIFO. Note-se que uma FIFO não é mais do que um ficheiro. Portanto, o acesso a uma FIFO é também definido pelas permissões de read and write para o proprietário, o grupo, e restantes utilizadores.

 

As pipetas e as FIFOs seguem as seguintes regras (e mais algumas) de leitura e escrita:

 

§  Se num acesso de leitura forem solicitados MENOS dados do que aqueles que estão efectivamente na pipeta/FIFO, os dados restantes são lá deixados para subsequentes leituras.

§  Se num acesso de leitura forem solicitados MAIS dados do que aqueles que estão efectivamente na pipeta/FIFO, só os dados que estão disponíveis são devolvidos.

§  Se um processo debita dados e não esgota a capacidade da pipeta/FIFO (a qual tem pelo menos 4096 bytes), a escrita é garantidamente atómica. Isto significa que se dois processos iniciam a escrita de dados  para a pipeta/FIFO mais ou menos na mesma altura, então há a garantia de que os dados de um dos processos são escritos após todos os dados do outro processo terem sido escritos. Não há mistura de dados dos dois processos na pipeta/FIFO. No entanto, se o débito dum dos processos ultrapassa a capacidade da pipeta/FIFO, então não há garantia de a operação de escrita ser atómica.

 

No Exemplo 11.6 usámos pipetas para fazer a comunicação entre um processo servidor e um processo cliente, em que um dos processos era filho do outro. De facto, as pipetas não podem ser partilhadas por processos independentes. Mas, com as FIFOs não há esta restrição.

 

Vejamos então o mesmo exemplo cliente-servidor implementado com dois processos independentes que partilham duas FIFOs como canal IPC de comunicação bidireccional:


Exemplo 11.9: (Cliente-servidor através de FIFOs) A compilação do programa está descrita no ficheiro Makefile da directoria $CURSO/stevens/unp/ipc

 

Alternativamente ao livro do Stevens pode-se usar o Makefile e procedimento a seguir

 

s_fifo:         s_fifoserv s_fifocli

 

s_fifoserv:     mainfifoserv.c sub_servfd.c error.c

        cc $(CFLAGS) -o server mainfifoserv.c sub_servfd.c error.c

 

s_fifocli:      mainfifocli.c  sub_clifd.c error.c

        cc $(CFLAGS) -o client mainfifocli.c sub_clifd.c error.c

 

Como se pode ver, este programa envolve os seguintes ficheiros:

·       mainfifoserv.c   - que contém o programa principal do processo servidor;

·       sub_servfd.c      - que contém a função server;

·       mainfifocli.c      - que contém o programa principal do processo cliente;

·       sub_clifd.c         - que contém a função client.

·       error.c              -que contém funções de erro

·       cs.h                  - que contêm definições comuns

·       fifo.h                 - que contêm definições comuns relativos aos ficheiros dos pipes.

 

 

O programa principal do cliente é o seguinte:

mainfifocli.c

 
 


#include  "cs.h"

#include  "fifo.h"

 

int main()

{

      int   readfd, writefd;

      umask(0);

     

       /* Open the FIFOs.  We assume the server has already created them. */

     

      if ( (writefd = open(FIFO1, 1)) < 0)

            err_sys2("client: can't open write fifo: %s", FIFO1);

      if ( (readfd = open(FIFO2, 0)) < 0)

            err_sys2("client: can't open read fifo: %s", FIFO2);

 

      client(readfd, writefd);

 

      close(readfd);close(writefd);

 

      /* Delete the FIFOs, now that we're finished */

 

      if (unlink(FIFO1) < 0)

            err_sys2("client: can't unlink %s", FIFO1);

      if (unlink(FIFO2) < 0)

            err_sys2("client: can't unlink %s", FIFO2);

 

      return(0);

}

O ficheiro fifo.h tem o conteúdo seguinte:

fifo.h

 
 


#include    <sys/types.h>

#include    <sys/stat.h>

#include    <sys/errno.h>

extern int  errno;

 

#define     FIFO1 "/tmp/fifo.1"

#define     FIFO2 "/tmp/fifo.2"

#define     PERMS 0666

 

O programa principal do servidor é o seguinte:

mainfifoserv.c

 
 


#include    "cs.h"

#include    "fifo.h"

 

int main()

{

      int   readfd, writefd;

 

      /* Create the FIFOs, then open them - one for reading and one

       * for writing.

       */

umask(0);

 

      if ( (mknod(FIFO1, S_IFIFO | PERMS, 0) < 0) && (errno != EEXIST))

            err_sys2("can't create fifo: %s", FIFO1);

      if ( (mknod(FIFO2, S_IFIFO | PERMS, 0) < 0) && (errno != EEXIST)) {

            unlink(FIFO1);

            err_sys2("can't create fifo: %s", FIFO2);

      }

 

      if ( (readfd = open(FIFO1, 0)) < 0)

            err_sys2("server: can't open read fifo: %s", FIFO1);

      if ( (writefd = open(FIFO2, 1)) < 0)

            err_sys2("server: can't open write fifo: %s", FIFO2);

 

      server(readfd, writefd);

 

      close(readfd);

      close(writefd);

 

      return(0);

}

 

O processo servidor (chamado server) deve iniciar a sua execução em primeiro lugar e em bastidor (ou background) da seguinte forma:

 

$ server &

 

O processo cliente (chamado client) deve ser executado de seguida em primeiro plano (visto que o cliente lê da entrada estandardizada e escreve para a saída estandardizada):

 

$ client


Filas de mensagensF

 

A comunicação através de mensagens é agora usual nos sistemas operativos modernos.

 

Todas as mensagens são armazenadas no núcleo (kernel) do sistema operativo, cada uma das quais inclui o identificador da fila a que está associada.

 

Os processos lêem e escrevem mensagens de e para filas arbitrárias. Não existe qualquer requisito que impeça que um processo que se encontra à espera duma mensagem específica não possa escrever uma mensagem para a mesma fila. Isto contrasta com as pipetas e as FIFOs, para as quais não faz sentido ter um processo escritor a não ser que exista um processo leitor. É bem possível que um processo escreva algumas mensagens para a fila, termine a escrita e continue a executar outras tarefas, e só mais tarde as ditas mensagens serem lidas por outro processo.

 

Qualquer mensagem tem os seguintes atributos:

 

§  type (long int)

§  length (comprimento dos dados da mensagem)

§  data (se length não é nulo)

 

Qualquer fila de mensagens tem a seguinte estrutura:

 

#include <sys/types.h>

#include <sys/ipc.h>          /* defines the ipc_perm structure*/

 

struct msqid_ds {

      struct      ipc_perm    msg_perm;   /* op. permission struct */

      struct      msg         *msg_first; /* ptr to first message on q*/

      struct      msg         *msg_last;  /* ptr to last message on q*/

      ushort                  msg_cbytes; /* current no. bytes on q */

      ushort                  msg_qnum;   /* current no. messages on q */

      ushort                  msg_qbytes; /* max no. bytes allowed on q */

      ushort                  msg_lspid;  /* pid of last msgsnd*/

      ushort                  msg_lrpid;  /* pid of last msgrcv*/

      time_t                  msg_stime;  /* time of last msgsnd*/

      time_t                  msg_rtime;  /* time of last msgrcv*/

      time_t                  msg_ctime;  /* pid of last msgctl*/

};

 

A estrutura ipc_perm contém as permissões de acesso a uma fila de mensagens. Cada estrutura msg define uma mensagem numa lista ligada em que msg_first e msg_last são apontadores para a primeira e última mensagens, respectivamente.

 

Por exemplo, na Figura 11.9 representa-se uma fila com três mensagens cujos comprimentos são de 1 byte, 2 bytes e 3 bytes, respectivamente. Os tipos das mensagens são 100, 200 e 300, respectivamente.

 

 

 

 

 

 


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


Figura 11.9: Fila de mensagens no núcleo do sistema operativo.

 

 

Criação/Acesso a fila de mensagens

 

Uma fila de mensagens é criada (ou acedida) através da seguinte chamada ao sistema:

 

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

 

int msgget(key_t key, int msgflag);

 

Retorna: identificador da fila de mensagens, -1 em caso de erro.

 

 

O valor de msgdflag é uma combinação das constantes da Figura 11.10, as quais definem o modo de acesso à fila.

 

Numérico

Simbólico

Descrição

0400

MSG_R

Read by owner

0200

MSG_W

Write by owner

0040

MSG_R >> 3

Read by group

0020

MSG_W >> 3

Write by group

0004

MSG_R >> 6

Read by world

0002

MSG_W >> 6

Write by world

 

IPC_CREAT

 

 

IPC_EXCL

 

 

Figura 11.10: Valores de msgflag na chamada de msgget.

 

 

 


Emissão de mensagem

O envio duma mensagem para uma fila de mensagens é feita através da seguinte chamada ao sistema:

 

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

 

int msgsnd(int msqid, struct msgbuf *ptr, int length, int flag);

 

Retorna: 0 se OK, -1 em caso de erro.

 

Expliquemos os argumentos da função anterior:

 

·       msqid. É o identificador da fila de mensagens.

·       ptr. É um endereçador duma estrutura msgbuf (veja-se <sys/msg.h>) que contém dois campos: o primeiro mtype indica o tipo da mensagem, e o segundo mtext é um array que contém os dados da mensagem. Os dados podem ser textuais ou binários.

·       length. Especifica o comprimento da mensagem em bytes. O comprimento pode ser zero.

·       flag. Toma os valores IPC_WAIT ou zero. IPC_WAIT permite o retorno imediato da função no caso de não existir espaço na fila para a nova mensagem. Se não existe espaço na fila, e se IPC_WAIT é especificado, msgsnd devolve o valor –1 e errno toma o valor EAGAIN. Se esta chamada ao sistema é feita com sucesso, ela devolverá o valor zero.

 

Recepção de mensagem

Uma mensagem é lida a partir duma fila de mensagens através da seguinte chamada ao sistema:

 

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

 

int msgrcv(int msqid, struct msgbuf *ptr, int length, long msgtype, int flag);

 

Retorna: tamanho da porção de dados da mensagem se OK, -1 em caso de erro.

 

·       msqid. Identifica a fila de mensagens.

·       ptr. É um endereçador duma estrutura msgbuf (veja-se <sys/msg.h>) que contém a mensagem.

·       length. Especifica o comprimento da mensagem em bytes.

·       msgtype. Especifica qual é a mensagem que se pretende ler da fila:

q  Se msgtype=0, a primeira mensagem da fila é devolvida. Dado que a fila segue uma política FIFO de escalonamento, a mensagem mais antiga é devolvida.

q  Se msgtype>0, a primeira mensagem com o tipo igual a msgtype é devolvida.

q  Se msgtype<0, a primeira mensagem com o tipo mais baixo que é menor ou igual ao valor absoluto de msgtype é devolvida.

·       flag. Especifica o que fazer no caso duma mensagem dum dado tipo não se encontrar na fila. Se o bit IPC_WAIT estiver activo, msgrcv faz retorno imediato no caso de a mensagem não estar disponível. Neste caso, msgrcv devolve o valor –1 e errno toma o valor ENOMSG. Caso contrário, o processo invocador é suspendido até um dos seguintes eventos ocorra:

q  Uma mensagem do tipo pedido está disponível.

q  A fila de mensagens é removida do sistema.

q  O processo recebe um sinal que é capturado.

Se tudo correr bem, msgrcv devolve o número de bytes dos dados existentes na mensagem recebida.


Controlo da fila de mensagens

 

As operações de controlo sobre a fila de mensagens são feitas através da seguinte chamada ao sistema:

 

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/msg.h>

 

int msgctl(int msqid, int cmd, struct msqid_ds *buff);

 

Retorna: 0 se OK, -1 em caso de erro.

 

No exemplo que se segue só usaremos o valor IPC_RMID em cmd para remover uma fila de mensagens do sistema operativo.

 

Exemplo 11.10: (Cliente-Servidor através de 2 filas de mensagens)

A compilação do programa está descrita no Makefile da directoria $CURSO/stevens/unp/ipc como se segue:

 

tudo:        filas_server  fila_cliente

 

file_server:     mainmsgqserv.c sub_servmesg.c sub_mesgmsgq.c $(MYLIB)

        cc $(CFLAGS) -o server mainmsgqserv.o sub_servmesg.o sub_mesgmsgq.o $(MYLIB)

 

fila_cliente:      mainmsgqcli.c sub_climesg.c sub_mesgmsgq.c $(MYLIB)

        cc $(CFLAGS) -o client mainmsgqcli.o sub_climesg.o sub_mesgmsgq.o $(MYLIB)

 

onde $(MYLIB) é a biblioteca libnet.a.

Como se pode ver, este programa envolve os seguintes ficheiros:

·       mainmsgqserv.c - que contém o programa principal do processo servidor;

·       sub_servmesg.c - que contém a função server;

·       mainmsgqcli.c   - que contém o programa principal do processo cliente;

·       sub_climesg.c   - que contém a função client;

·       sub_mesgmsgq.c – que contêm rotinas comuns de envio e receber

 

O programa principal do servidor é o seguinte:

mainmsgqserv.c

 
 


#include    "msgq.h"

 

main()

{

      int   readid, writeid;

 

      /* Create the message queues, if required. */

 

      if ( (readid = msgget(MKEY1, PERMS | IPC_CREAT)) < 0)

            err_sys("server: can't get message queue 1");

      if ( (writeid = msgget(MKEY2, PERMS | IPC_CREAT)) < 0)

            err_sys("server: can't get message queue 2");

 

      server(readid, writeid);

 

      exit(0);

}

 

O programa principal do cliente é o seguinte:

mainmsgqcli.c

 
 


#include    "msgq.h"

 

main()

{

      int   readid, writeid;

 

      /*

       * Open the message queues.  The server must have

       * already created them.

       */

 

      if ( (writeid = msgget(MKEY1, 0)) < 0)

            err_sys("client: can't msgget message queue 1");

      if ( (readid = msgget(MKEY2, 0)) < 0)

            err_sys("client: can't msgget message queue 2");

 

      client(readid, writeid);

 

      /*

       * Now we can delete the message queues.

       */

 

      if (msgctl(readid, IPC_RMID, (struct msqid_ds *) 0) < 0)

            err_sys("client: can't RMID message queue 1");

      if (msgctl(writeid, IPC_RMID, (struct msqid_ds *) 0) < 0)

            err_sys("client: can't RMID message queue 2");

 

      exit(0);

}

 

 

 


A função server é agora a seguinte:

sub_servmesg.c

 
 


#include    <stdio.h>

#include    "mesg.h"

 

extern int  errno;

Mesg  mesg;

 

int server(int ipcreadfd, int ipcwritefd)

{

      int   n, filefd;

      char  errmesg[256], *sys_err_str();

 

      /* Read the filename message from the IPC descriptor. */

 

      mesg.mesg_type = 1L;

      if ( (n = mesg_recv(ipcreadfd, &mesg)) <= 0)

            err_sys("server: filename read error");

      mesg.mesg_data[n] = '\0';     /* null terminate filename */

 

      if ( (filefd = open(mesg.mesg_data, 0)) < 0) {

            /*

             * Error.  Format an error message and send it back to the client.

             */

                sprintf(errmesg, ": can't open, %s\n", sys_err_str());

            strcat(mesg.mesg_data, errmesg);

            mesg.mesg_len = strlen(mesg.mesg_data);

            mesg_send(ipcwritefd, &mesg);

 

      } else {

             /* Read the data from the file and send a message to

                the IPC descriptor.

             */

            while ( (n = read(filefd, mesg.mesg_data, MAXMESGDATA)) > 0) {

                  mesg.mesg_len = n;

                  mesg_send(ipcwritefd, &mesg);

            }

            close(filefd);

 

            if (n < 0)

                  err_sys("server: read error");

      }

 

      /* Send a message with a length of 0 to signify the end. */

 

      mesg.mesg_len = 0;

      mesg_send(ipcwritefd, &mesg);

}

 

 

 

 

 

 

… ao passo que a função client é a seguinte:

sub_climesg.c

 
 


#include    <stdio.h>

#include    "mesg.h"

 

Mesg  mesg;

 

int client(int ipcreadfd, int ipcwritefd)

{

      int   n;

 

      /*

       * Read the filename from standard input, write it as

       * a message to the IPC descriptor.

       */

 

      if (fgets(mesg.mesg_data, MAXMESGDATA, stdin) == NULL)

            err_sys("filename read error");

 

      n = strlen(mesg.mesg_data);

      if (mesg.mesg_data[n-1] == '\n')

            n--;              /* ignore newline from fgets() */

      mesg.mesg_len = n;

      mesg.mesg_type = 1L;

      mesg_send(ipcwritefd, &mesg);

 

      /*

       * Receive the message from the IPC descriptor and write

       * the data to the standard output.

       */

 

      while ( (n = mesg_recv(ipcreadfd, &mesg)) > 0)

            if (write(1, mesg.mesg_data, n) != n)

                  err_sys("data write error");

 

      if (n < 0)

            err_sys("data read error");

}

 

 

 


Por sua vez, as funções server e client invocam as funções mesg_send e mesg_recv abaixo descritas:

 

sub_mesgmsgq.c

 
 


#include    "mesg.h"

 

/*

 * Send a message using the System V message queues.

 * The mesg_len, mesg_type and mesg_data fields must be filled

 * in by the caller.

 */

 

mesg_send(int id, Mesg  *mesgptr)

{

      /*

       * Send the message - the type followed by the optional data.

       */

 

      if (msgsnd(id, (char *) &(mesgptr->mesg_type),

                              mesgptr->mesg_len, 0) != 0)