Unidade de Gestão da Educação Presencial - GEDUP Pós-graduação em Redes de Computadores Sistemas Distribuídos (Parte 4 - Aplicação Distribuída) Prof. Ms. Tomás Dias Sant Ana Varginha, 2006
Sumário 1. INTRODUÇÃO...1 2. APLICAÇÃO DISTRIBUÍDA...2 2.1 COMPUTAÇÃO DISTRIBUÍDA...2 3. TÉCNICAS PARA IMPLEMENTAÇÃO DE APLICAÇÕES DISTRIBUÍDAS...5 3.1 ARQUITETURA CLIENTE/SERVIDOR...5 3.1.1 O protocolo TCP-IP...7 3.1.2 O endereço IP...8 3.1.3 Portas...8 3.2 Sockets...9 3.2.1 Criação de um socket...10 3.2.2 Stream Socket...11 3.2.2 Datagrama Socket...14 3.4 Programação Multi-Threading...16 8. BIBLIOGRAFIA...20
1 1. Introdução Até pouco tempo, na construção das aplicações monolíticas que rodam nos mainframes, o projetista tinha pouca ou quase nenhuma preocupação com a arquitetura da aplicação. Com a viabilização da rede e das tecnologias de comunicação e processamento distribuído, determinar a arquitetura de uma aplicação passou a ser uma atividade estratégica na construção de sistemas e inclui decidir sobre o seu particionamento e a localização de suas partes. O crescimento das redes de computadores e meios de comunicação, bem como a heterogeneidade das plataformas de hardware e software impulsionou a investigação científica de aspectos como abertura, transparência, mobilidade do usuário, confiabilidade, interoperabilidade e outros, todos inerentes aos sistemas distribuídos (Tanenbaun, 1996; Coulouris et al., 1994). Em adição, o advento da Internet, em termos de uma grande rede mundial que engloba plataformas de hardware e software bastante heterogêneas, impulsionou ainda mais o desenvolvimento dos sistemas distribuídos, embora nem todos os conceitos essenciais de sistemas distribuídos estejam presentes na Internet. Fornecido o suporte à conexão das máquinas e também o suporte operacional, é relevante investigar os recursos providos pelas linguagens de programação e/ou ambientes de desenvolvimento, em termos da implementação de aplicações distribuídas. Existem várias linguagens que fornecem suporte ao desenvolvimento de aplicações, geralmente através de bibliotecas que contemplam recursos para o estabelecimento de conexões entre computadores, transferência de mensagens, controle dos aspectos envolvidos na comunicação e outros. A heterogeneidade das plataformas de hardware e software é um ponto positivo para o uso de sistemas distribuídos, visto que a integração dos diferentes ambientes operacionais é um dos focos de investigação científica da área. No entanto, ao nível dos desenvolvedores de aplicações (programadores), o desenvolvimento de aplicações que possam ser executadas em diferentes ambientes operacionais é um grande desafio, pois a maioria das linguagens de programação está ligada diretamente a um determinado tipo de sistema operacional.
2 2. Aplicação Distribuída Aplicações distribuídas são aquelas em que existem programas que executam simultaneamente em vários computadores interligados em rede. Para que os diferentes programas, que constituem a aplicação distribuída, possam comunicar entre si precisam obedecer a regras, designadas por protocolos. A complexidade da especificação desses protocolos e mesmo a necessidade de reutilização de software fizeram com que esses protocolos fossem estruturados em famílias de protocolos, com uma determinada arquitetura. Uma família de protocolos, também designada por pilha de protocolos, caracteriza-se por uma arquitetura e pelo fato dos seus protocolos serem aprovados por uma mesma entidade. Desde o inicio, os vários fabricantes se lançaram a definir as suas famílias de protocolos: SNA (IBM), DNA (Digital), etc. Mas, cedo se constatou um problema: como executar aplicações distribuídas em ambientes heterogêneos? É naturalmente de todo interesse que estes tipos de aplicações possam ser usados em ambientes heterogêneos, isto é, ambientes onde coexistam computadores de fabricantes diferentes, com sistemas operacionais diferentes. Para que isso seja possível, precisam existir famílias de protocolos normalizadas internacionalmente, independentemente dos fabricantes. Existem duas famílias de protocolos com esse estatuto: a família de protocolos OSI, normalizada conjuntamente pela ISO e pelo CCITT; os protocolos desta família são divulgadas por normas ISO e recomendações do CCITT; a família de protocolos Internet (TCP/IP, descrito no item 3.1.2), normalizada pelo IAB (Internet Activities Board); os protocolos desta família são divulgados como RFC's (Request For Comments). 2.1 Computação Distribuída
3 A computação distribuída divide a carga de processamento entre dois ou mais computadores. No entanto, as coisas não são tão simples. Hoje temos que olhar além dos conceitos tradicionais de computação distribuída, partindo do mundo two-tier (i. e. clienteservidor) para um mundo mais complexo, onde aplicações são divididas entre múltiplos processadores e bancos de dados. Ainda mais, estas aplicações precisam manter as suas características de missão crítica tais como tolerância à falhas e recuperação de erros. Para entender a integração de aplicações multitiered (múltiplos pontos), precisamos entender a arquitetura. O particionamento de aplicações é realmente um conceito de arquitetura, onde a tecnologia (por exemplo, objetos distribuídos) é um mero mecanismo para implementação de uma aplicação particionada. Uma decisão fundamental para a construção de um sistema distribuído é a arquitetura base a ser utilizada. Uma vez selecionada a arquitetura base, o projetista precisa decidir onde colocar os processos da aplicação para melhor otimizar os recursos disponíveis e atender os requisitos da aplicação. Há muitas coisas a serem consideradas, incluindo: carga do usuário; carga do processamento; tipo da aplicação; custo de desenvolvimento; manutenção; expectativas de desempenho. Como se cria uma arquitetura? Antes de se optar por uma arquitetura, algumas atividades precisam ser executadas como entender os requisitos em detalhes, criar o projeto da aplicação e considerar todos os itens acima mencionados. Um dos erros mais comuns que os arquitetos cometem é abordar o problema com uma arquitetura pré-definida e tentar fazer o problema se ajustar a ela. Não é uma boa idéia. O mais recomendável é selecionar a arquitetura que melhor se ajuste ao problema em mãos. O primeiro passo é criar uma arquitetura lógica. Uma arquitetura lógica permite o mapeamento de certas funções da aplicação em camadas da arquitetura conceitual, sem comprometimento com uma determinada camada ou tecnologia disponível. Assim como no
4 projeto lógico de banco de dados, é recomendável entender os objetivos e metas da arquitetura antes de entrar nos seus detalhes. Agora que se tem uma arquitetura lógica, é hora de concentrar as atenções para o mapeamento do lógico para o físico. Antes é necessário selecionar as tecnologias de apoio como sockets, ferramentas de particionamento de aplicações, ferramentas especializadas de computação distribuída, servidores de banco de dados ou objetos distribuídos. A tecnologia de apoio definirá como os objetos lógicos serão fisicamente implementados.
5 3. Técnicas para Implementação de Aplicações Distribuídas Os Sistemas Distribuídos podem ser caracterizados como sendo um conjunto de computadores autônomos, interligados por uma rede de comunicação, e que permitem o compartilhamento dos recursos do sistema, tais como hardware, software e dados. Em adição, o acesso aos recursos oferecidos por um sistema distribuído deve ser transparente aos olhos do usuário, ou seja, independente de sua localização, o usuário deve acessar os serviços da mesma forma, tendo a impressão de que o sistema é o seu computador. Atualmente a maioria dos sistemas distribuídos baseia-se na arquitetura clienteservidor e no uso do protocolo TCP-IP. 3.1 Arquitetura Cliente/Servidor A organização dos sistemas distribuídos deve considerar os aspectos lógicos, tais como estrutura lógica do sistema, relação usuário/sistema, conceito de clientes-servidores e outros; e os aspectos físicos, tais como estrutura física, organização do hardware, elementos básicos e outros. De modo geral, os modelos arquiteturais dos sistemas distribuídos podem ser classificados em clássicos, compostos e avançados. Os modelos arquiteturais clássicos compreendem o modelo de minicomputadores, o modelo estação de trabalho-servidor e modelo de banco de processadores. Atualmente, o modelo clássico baseado em estações de trabalho-servidor, conforme ilustra a Figura 3.1, tem sido o mais difundido por apresentar uma arquitetura simplificada e flexível, que se adapta às necessidades da maioria dos usuários.
6 Estações de Trabalho e Computadores Pessoais Rede de Comunicação Roteador (ligação com uma WAN) Servidores de Arquivos Outros Servidores Figura 3.1 Modelo arquitetural estação de trabalho-servidor O modelo estação de trabalho-servidor é caracterizado pela presença de um ou mais servidores e várias estações de trabalho (clientes). O servidor é responsável pelo gerenciamento e disponibilização dos recursos às estações de trabalho que, por sua vez, realizam os pedidos. De modo geral, o processo de comunicação do cliente e servidor envolve um meio de transmissão e um protocolo de comunicação, por exemplo UDP (User Datagram Protocol) ou TCP (Transfer Control Protocol). O TCP é um protocolo baseado em conexão que prove um fluxo de dados confiável entre dois computadores, exemplos: HTTP, FTP, Telnet. E UDP é um protocolo que envia pacotes de dados independentes, chamados datagramas, de um computador para o outro, exemplo: Ping. Utilizando o protocolo TCP, o servidor possui um módulo de software que permanece sempre em execução (listening) para atender às chamadas dos clientes. Uma conexão é criada com o servidor sempre que um cliente solicita algum serviço. Em adição, o servidor é capaz de manter várias conexões com os clientes simultaneamente, conforme ilustra a Figura 3.2. Os pedidos dos clientes chegam ao servidor em um endereço lógico, denominado porta, que identifica o tipo de serviço a ser realizado. Após o recebimento da solicitação, o
servidor executa o serviço, "empacota" o resultado e envia de volta ao cliente, utilizando o mesmo endereço lógico. 7 Cliente 1 Servidor Cliente 2 Cliente 3 Figura 3.2 - Esquema genérico de uma conexão cliente-servidor 3.1.1 O protocolo TCP-IP Atualmente, a Internet apresenta-se como o mais difundido modelo de comunicação cliente-servidor. Através dela é possível interligar redes do mundo inteiro, utilizando-se para isto um protocolo de comunicação padrão, denominado TCP/IP (Transfer Control Protocol - Internet Protocol), e computadores intermediários, chamados routers ou gateways, que são responsáveis pela troca (comutação) de pacotes entre as redes (Commer, 1991; Thomas, 1997). De modo geral, cada rede possui um endereço IP, utilizado para identificá-la dentre as demais redes da Internet. Em adição, cada placa de rede de um computador possui um endereço único determinado pelo fabricante, que é utilizado para a localização do computador dentro de uma rede. Assim, ao enviar um pacote de um computador a outro via Internet, deve-se acrescentar um endereço lógico, que inclui o endereço da rede e o endereço do computador. Os routers utilizam o processo de ARP (Address Resolution Protocol) para localizar o computador de destino. Este processo consiste em receber o pacote, verificar se o endereço lógico faz parte da lista de endereços de rede que ele possui, enviar um broadcast a todos os computadores da rede e, após a identificação do endereço físico, enviar o pacote ao computador.
8 3.1.2 O endereço IP Conforme citado, o endereço IP é um endereço lógico utilizado para a localização de um computador em uma rede TCP/IP. Basicamente, o endereço IP é um número de 32 bits composto por quatro conjuntos de oito bits (quatro bytes), como por exemplo 10110001 00101111 00010110 101110100, que podem ser traduzidos para os conjuntos numéricos 136.47.238.169 (Thomas, 1997). Para facilitar a tarefa de roteamento, os endereços IP são hierárquicos. Desta forma, ao receber um pacote, o router pode identificar se este é ou não para a sua rede. Como exemplo, se uma rede possui o endereço IP 135.10.0.0, todos os seus computadores devem possuir endereço do tipo 135.10.x.x. Com o objetivo de facilitar a localização de um servidor (host), o endereço IP é traduzido para um nome, por exemplo "unifenas.br". O processo inverso, ou seja, a tradução do nome para o endereço IP é denominada Hostname Resolution (resolução de nomes). O sistema de roteamento verifica a existência de um nome em tabelas de DNS (Domain Name System) mantidas pelos diversos mecanismos roteadores (routers), através dos endereços IP (Internet Protocol). Caso o servidor seja encontrado e esteja em operação, uma interface é fornecida ao usuário para acesso aos serviços disponíveis. 3.1.3 Portas Conforme citado, a comunicação pela Internet não é simplesmente a comunicação entre dois computadores, mas sim entre duas aplicações. Para que isto seja possível, a mensagem enviada pela aplicação cliente deve conter uma identificação da aplicação servidora, neste caso, o endereço lógico denominado porta. Em adição, a aplicação cliente deve enviar o número da porta para recebimento da resposta. Todas estas informações são adicionadas ao header do pacote. O endereço lógico deve possuir um número de 16 bits na faixa entre 0 (zero) e 65535, sendo que as portas de número inferior a 1024 (entre 0 e 1023) são reservadas para aplicações padrões, por exemplo HTTP, que utiliza a porta 80; FTP, a porta 21; Telnet, a porta 23; e outras (figura 3.3).
9 Figura 3.3 - Esquematização das Portas 3.2 Sockets Sockets são os componentes básicos para comunicação entre processos, prove acesso ao protocolo de transporte de rede. Os dois principais tipos de sockets são: Stream Socket: um stream socket prove uma comunicação bidirecional, confiável, seqüencial e não duplicada. Utiliza o protocolo TCP/IP. Datagram Socket: prove a comunicação bidirecional de dados, o dado é dividido em pacotes, as mensagens podem ser recebidas fora de ordem e ou duplicadas. Ele usa o protocolo UDP. O estabelecimento da conexão é assimétrico, com um processo atuando como cliente e outro como servidor: O servidor estabelece (bind) um socket para um endereço conhecido. Ele bloqueia o socket para um pedido de conexão. O processo pode conectar com o servidor.
10 3.2.1 Criação de um socket O primeiro passo é criar o socket, através da função socket(): s = socket(domínio, tipo, protocolo) onde, Domínio: é especificado por uma das constantes definidas em <sys/socket.h>. Para o UNIX o domínio é AF_UNIX e para a Internet o domínio é AF_INET. Tipo: são suportados três tipos de sockets: SOCK_STREAM para stream sockets, SOCK_DATAGRAM para datagram sockets e SOCK_RAW para Row Sockets.Protocolo: O argumento 0 deve ser usado na maioria dos casos. O sistema seleciona um protocolo que suporta o tipo de requisição. O socket é criado sem um nome, mas deve-se especificar o nome para que a comunicação possa ocorrer entre as aplicações. A função bind() permite o processo especificar um meio de comunicação: bind (s, nome, tamanho_do_nome) S: é o handle do socket. Nome: é uma estrutura que é interpretada por um protocolo. Exemplo: o domínio Internet contém um Internet Address e o número da porta. Tamanho do Nome: é o tamanho da estrutura nome Exemplo Internet biding: int sock; struct sockaddr_in Nome; Nome.sin_family = AF_INET; Nome.sin_addr.s_addr = INADDR_ANY; Nome.sin_port = 0; bind (sock, (struct sockaddr *)&Nome, sizeof(nome)); Solicitação (ouvindo) e Communicating (comunicando) Pedido canal: servidor apenas escuta o pedido. Canal de comunicação: é criado para cada comunicação.
11 Figura 3.4 - Representação da comunicação utilizando Strem Socket 3.2.2 Stream Socket Lado do Cliente No lado cliente a função connect() inicia uma conexão, e automaticamente seleciona um nome (bind) para o socket: int Sock; struct sockaddr_in Servidor; struct hostent *hp;... Servidor.sin_family= AF_INET; hp= gethostbyname("host name"); memcpy((char*) & Servidor.sin_addr, (char*) hp->h_addr, hp->h_length); Servidor.sin_port= htons(port_number);... connect(sock, (struct sockaddr*) &Servidor, sizeof(servidor)); A função gethostbyname(nome_host) retorna uma estrutura incluíndo o endereço de rede do Host (NOME_HOST). Lado do Servidor Para receber uma conexão cliente, o servidor deve realizar os seguintes passos após o binding (Figura 3.4): 1. Indicar quantas conexões podem ser colocadas em fila (listen()). 2. Aceitar a conexão (accept()). int Sock; struct sockaddr_in Cliente;... listen(s, 5);... Sock = accept(sock, (struct sockaddr *)&Cliente, sizeof(cliente)); Listen(S, 5) S é o endereço do socket para conexão, e o segundo parâmetro especifica o número máximo de conexões que podem ser colocadas em fila. Accept() retorna um novo socket que é conectado ao cliente.
Transferência de dados: Há várias funções para enviar e receber dados. A função socket() retorna um descritor de arquivo que pode ser usado com muitas chamadas I/O do UNIX, por exemplo: write(sock, mensagem, sizeof(buf)); read(sock, mensagem, sizeof(buf)); ou, send(sock, mensagem, sizeof(buf), flag); recv(sock, mensagem, sizeof(buf), flag); 12 Fechando um socket: O socket deve ser fechado usando a função close(). Exemplo: close(s). Exemplo Cliente: #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <strings.h> #define DATA "1 2 3 TESTANDO..." main (int argc, char* argv[]) { int sock; struct sockaddr_in server; struct hostent *hp; sock = socket (AF_INET, SOCK_STREAM, 0); if (sock == -1) { perror ("Opening stream socket"); exit (1); hp = gethostbyname(argv[1]); memcpy ((char*)&server.sin_addr, (char*) hp->h_addr, hp->h_length); server.sin_family = AF_INET; server.sin_port = htons(atoi(argv[2])); if (connect(sock, (struct sockaddr *)&server, sizeof server) == -1) { perror("connecting stream socket"); exit(1); if (write(sock, DATA, sizeof DATA) == -1) perror("writing data to stream socket"); close (sock); exit (0);
13 Exemplo Servidor: #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <strings.h> #define TRUE 1 main() { int sock, length; struct sockaddr_in server; int msgsock; char buf[1024]; int rval; sock = socket (AF_INET, SOCK_STREAM, 0); if (sock==-1) { perror( "Opening stream socket"); exit(1); server.sin_family = AF_INET; server.sin_addr.s_addr = INADDR_ANY; server.sin_port = 0; if (bind (sock, (struct sockaddr *)&server, sizeof(server)) == -1){ perror("binding stream socket"); exit (1); length = sizeof server; if (getsockname (sock, (struct sockaddr *)&server, &length) == -1){ perror("getting socket name"); exit (1); printf("socket port # %d\n", ntohs(server.sin_port)); /* Start accepting connections */ listen(sock, 5); do { msgsock = accept( sock, (struct sockaddr *) 0, (int*) 0); if (msgsock==-1) perror("accept"); else do { memset( buf, 0, sizeof buf); if (( rval = read( msgsock, buf, 1024)) == -1) perror("reading data stream"); if (rval == 0) printf("ending connection\n"); else printf( "-->%s\n", buf); while (rval!=0); close(msgsock); while (TRUE); exit(0);
14 3.2.2 Datagrama Socket Um datagram socket é criado como um stream socket, alterando apenas o flag que define o tipo do socket. Ele pode usar algumas chamadas para enviar e receber dados como no stream socket, exceto as funções accept() e listen(). As funções sendto() e recvfrom() são muito usadas para transferir dados em um datagram socket: sendto(s, buf, sizeof(buf), flag, &to, sizeof(to)); Envia o conteúdo de buf para o endereço indicado em &to. recvfrom(s, buf, sizeof(buf), flag, &from, sizeof(from)); Recebe o pacote e armazena em buf. Exemplo Cliente: #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <strings.h> #define DATA "Mensagem..." main (int argc, char* argv[]) { int sock; struct sockaddr_in server; struct hostent *hp; char buf[1024]; sock = socket (AF_INET, SOCK_DGRAM, 0); if (sock == -1) { perror ("Opening stream socket"); exit (1); hp = gethostbyname(argv[1]); memcpy ((char*)&server.sin_addr, (char*) hp->h_addr, hp->h_length); server.sin_family = AF_INET; server.sin_port = htons(atoi(argv[2])); if (sendto(sock, DATA, sizeof DATA, 0, (struct sockaddr *)&server, sizeof server) == -1) { perror("enviando dados"); exit(1); if (read(sock, buf, 1024) == - 1) perror("writing data to stream socket"); printf("teste: %s\n", buf); close (sock); exit (0);
15 Exemplo Servidor: #include <unistd.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <netdb.h> #include <stdio.h> #include <stdlib.h> #include <pthread.h> #include <strings.h> #define TRUE 1 main() { int sock, length; struct sockaddr_in server; struct sockaddr_in cliente; int msgsock; char buf[1024]; int rval; sock = socket (AF_INET, SOCK_DGRAM, 0); if (sock==-1) { perror( "Opening stream socket"); exit(1); server.sin_family = AF_INET; server.sin_addr.s_addr = INADDR_ANY; server.sin_port = 0; if (bind (sock, (struct sockaddr *)&server, sizeof(server)) == -1){ perror("binding stream socket"); exit (1); length = sizeof server; if (getsockname (sock, (struct sockaddr *)&server, &length) == -1){ perror("getting socket name"); exit (1); printf("socket port # %d\n", ntohs(server.sin_port)); do { if (recvfrom (sock, buf, 1024, 0, (struct sockaddr *)&cliente, &length) == -1) { perror ("Lendo pacote"); exit(1); if (sendto (sock, buf, 1024, 0, (struct sockaddr *)&cliente, sizeof cliente) == -1) { perror ("enviando pacote"); exit(0); printf("mensagem %s\n", buf); while (TRUE); exit(0);
16 3.4 Programação Multi-Threading No princípio da computação paralela, os termos concorrência e multiprogramação possuíam conceitos completamente distintos e eram aplicados a situações também distintas. Com a evolução do hardware e das técnicas de programação, tais termos passaram a ser complementares e, atualmente, são utilizados em conjunto para se construir técnicas híbridas para a computação paralela. Na programação seqüencial, os processos são executados um após o outro, enquanto que na programação concorrente os processos são executados concorrentemente. A Concorrência implica em mais de um processo iniciado e ainda não terminado, podendo ocorrer tanto em sistemas com um único processador (pseudoparalelismo multiprogramação), como em sistemas com vários processadores (programação paralela). Dentro desta definição, pode-se abstrair dois tipos de paralelismo: Pseudo-Paralelismo (Threads): os processos são executados de forma intercalada em um único processador, de maneira que apenas um está ativo a cada instante. A Figura 2.1 exemplifica este tipo de paralelismo, com a execução de três processos (e1, e2 e e3) em função do tempo. Processos e3 e3 e2 e2 e1 e1 t1 t Tempo Figura 3.1 Pseudo-Paralelismo Paralelismo Real: vários processos são executados no mesmo intervalo de tempo, ou seja, têm-se vários processadores podendo executar os processos simultaneamente (Figura 2.2). Se existem p processos e P processadores sendo executados concorrentemente, e p > P, forma-se uma situação de paralelismo misto.
17 Processos e3 e1 e2 Figura 3.2 Paralelismo Real O paralelismo real pode ser dividido em três tipos: paralelismo espacial (paralelismo real), temporal (pipeline) e combinado. t1 t Tempo O paralelismo espacial está relacionado à execução simultânea dos processos. O paralelismo temporal, ou pipeline, implica na execução de eventos sobrepostos no tempo. O processo é dividido em vários subprocessos, sendo cada um executado em um estágio especializado de hardware e software operando concorrentemente com os outros estágios do pipeline [HWA93] [NAV89]. Estágios Figura 3.3 Execução de um Pipeline A Figura 2.3 demonstra o funcionamento de um pipeline de quatro estágios (E1, E2, E3 e E4). Os estágios são organizados seqüencialmente de maneira que o primeiro estágio (E1) executa uma parte do processo P1 e, ao terminar, entrega o resultado para o segundo estágio que dará continuidade ao processamento. A cada unidade de tempo, um novo processo é iniciado, denominados P1, P2, P3..., Pn. Após quatro unidades de tempo, neste exemplo, o fluxo de processos completos torna-se contínuo, sendo que um processo é executado a cada unidade de tempo (paralelismo de eventos sobrepostos). No paralelismo combinado existem vários estágios de pipeline sendo executados em paralelo. E4 E3 E2 E1 P1 P2 P1 P2 P3 P1 P2 P3 P4......... P1 P2 P3 P4 P5... t1 t2 t3 t4 t5... t Tempo
18 Multiprogramação baseada em threads A multitarefa é a capacidade de se ter mais de um programa executando aparentemente ao mesmo tempo, podendo acontecer de duas maneiras, dependendo da capacidade de controle de tarefas do sistema operacional. Num primeiro caso, quando o sistema operacional sempre insiste na continuidade de um programa, a multitarefa é dita preemptiva. Por outro lado, quando o sistema operacional opta para que o programa coopere com outros programas, a multitarefa é dita cooperativa. A programação multiple threads estende ainda mais a idéia de multitarefa, fazendo com que dentro de um mesmo programa existam várias tarefas executando ao mesmo tempo. Neste contexto, cada fluxo de controle dentro de um programa é denominado thread. Desta forma, quando um programa possuir mais de uma unidade computacional é denominado multithreading. Um thread pode estar em cinco estados diferentes: Executando (Runnable): o thread está sendo executado. Ativo (Active): o thread está pronto para ser executado. Dormindo (Sleeping): o thread está dormindo, após sua execução. Stopped (Parado): o thread está parado esperando por algum recurso. Zombie (Zumbi): o thread foi finalizado. O exemplo abaixo implementa a aplicação socket (apresentada no capítulo anterior) utilizando multithreading.
19 pthread_t Servico;... listen(sock, 5); do { NovoSocket = accept(sock, (struct sockaddr *) 0, (int*) 0); if (NovoSocket==-1) perror("accept"); else { if (pthread_create(&servico, NULL, Servicos, NovoSocket)) printf("erro: Criando Thread\n"); pthread_join(servico, 0); while (1==1); close(sock); void* Servicos(void* Sock) { int TamanhoEnd, N; char Mensagem[1024]; char* Retorno; do { memset(mensagem, 0, sizeof Mensagem); if ((N = read(sock, Mensagem, 1024)) == -1) perror("lendo Dados"); if (N!= 0) printf("servchat>%s", Mensagem); ///Parser Retorno="TESTE"; if (send(sock, Retorno, strlen(retorno)+1, 0) == -1) perror("enviando Dados"); while (N!=0);... Sincronização As técnicas que garantem a comunicação e o acesso a recursos compartilhados são conhecidas como mecanismos de sincronização. A área de dados compartilhados pelos threads é conhecida como região crítica. Os mecanismos de sincronização devem evitar a concorrência nas regiões críticas, permitindo que somente um processo tenha acesso em um determinado instante. Esta idéia de exclusividade de acesso é denominada exclusão mútua. Em sistemas com memória compartilhada existem vários mecanismos para garantir a exclusão mútua, entre eles: mutex, semáforos e monitores. O exemplo abaixo apresenta o programa do produtor/consumidor utilizando threads mutex.
20 Pthread_mutex_t ContaMutex; int Contador;... IncrementaCotnador() { pthread_mutex_lock(&contamutex); Contador = Contador+1; pthread_mutex_unlock(&contamutex); int PegaContador() { int Aux; pthread_mutex_lock(&contamutex); Contador = Contador 1; Aux = Contador; pthread_mutex_unlock(&contamutex); return (Aux) 8. Bibliografia (Commer, 1991) Commer, D. E.: INTERNTEWORKING WITH TCP-IP - PRINCIPLES, PROTOCOLS E ARCHITECTURE. v.3, 1991. (Coulouris et al., 1994) Coulouris, G., Dollimore, J., Kindberg, T.: DISTRIBUTED SYSTEMS - CONCEPTS AND DESIGN. 2 nd ed., Addison- Wesley Publishing Company, 1994. (Huges, 1996) HUGES, Merlin - JAVA, Network Programming, Manning,1996 (Tanenbaun, 1996) (Thomas, 1997) Tanenbaun, A.: DISTRIBUTED OPERATING SYSTEMS. Prentice Hall, 1996. Thomas, M. D. et al.: PROGRAMANDO EM JAVA PARA A INTERNET. Editora Makron Books, São Paulo, 1997.