segunda-feira, 13 de julho de 2009

Experimentos para testar os limites do sistema

Os limites do sistema podem ser levantados utilizando a função ulimit. Basta digitar ulimit para verificar os parâmetros que podem ser passados e então encontrar o valor que o sistema limita.
Os experimentos criados são para testar o quanto esses valores são seguidos, para isso foram criados alguns programas para essa análise. É importante ressaltar que os programas descritos podem trazer instabilidade para o sistema, por isso optou-se por realizar os testes numa máquina de testes (máquina virtual criada com o VirtualBox da Sun) com 512 MB de memória física.

Quantos processos podem ser criados?
Para este teste, utilizou-se a chamda fork em um loop infinito, que termina com o lançamento de uma exceção quando mais nenhum fork pode ser chamado.

#include <stdio.h>
#include <stdlib.h>

int main(){
int i=0;
while(fork()>0){
i++;
}
FILE* arquivo;
arquivo = fopen("registro","w+");
fprintf(arquivo, "Processo: %d\n", i);
fclose(arquivo);
sleep();
}
A saída para a execução deste programa foi:
Processo: 7077
O resultado foi obtido do arquivo registro após reiniciar o computador, uma vez que a execução do programa causou o travamento do computador. E o número indica, portanto, aproximadamente o número máximo de processos que podem ser criados.

Qual é o maior tamanho de processo?
$ ulimit -a
core file size (blocks, -c) 0
data seg size (kbytes, -d) unlimited
scheduling priority (-e) 20
file size (blocks, -f) unlimited
pending signals (-i) 16382
max locked memory (kbytes, -l) 64
max memory size (kbytes, -m) unlimited
open files (-n) 1024
pipe size (512 bytes, -p) 8
POSIX message queues (bytes, -q) 819200
real-time priority (-r) 0
stack size (kbytes, -s) 8192
cpu time (seconds, -t) unlimited
max user processes (-u) unlimited
virtual memory (kbytes, -v) unlimited
file locks (-x) unlimited
Qual é a maior área de heap?
A área de heap é a área que o sistema reserva para cada processo alocar memória dinamicamente, portanto o teste natural para encontrar este limite foi utilizar a chamada malloc da mesma maneira que com o fork.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MB 1024*1024

int main()
{
void *ponteiro = NULL;
int i = 0;

while(1)
{
ponteiro = (void *) malloc(MB);
if (!ponteiro) break;
memset(ponteiro,1, MB);
printf("Memória alocada: %d MB\n",++i);
}
exit(0);

}
O resultado obtido foi com a execução do programa compilado expressa o tamanho da heap:
Memória alocada: 569 MB
Killed
Esta era apresentada como ilimitada na chamada de sistema ulimit -d!

Qual é a maior área de pilha?
Toda vez que se chama uma função, o processo guarda o endereço atual na pilha e vai para o endereço da função, por isso o teste utilizado foi fazer uma função que chama a si mesma indefinidamente.
#include <stdio.h>
int i;

void funcao(){
printf("Chamada %d\n",++i);
funcao();
}

int main(){
i = 0;
funcao();
}
A execução do programa acima apresentou uma saída final:
Chamada 523826
Segmentation fault

O que significa que, se considerarmos que são 8192kB de pilha, temos que cada chamada de função armazena 16B de dados na pilha que, de fato, equivale ao tamanho do endereço.

A área de swap pode ser esgotada?
A área de swap pode ser esgotada e um dos efeitos que isso causa é o trashing da memória, isto é, os processos são retirados e recolocados na memória a ponto de mal conseguirem manter seu caminho normal de execução. Dessa maneira, o tempo de processamento aumenta muito e torna a dificulta ainda mais a liberação de memória, já que menos processos conseguem terminar sua execução.
Uma solução que foi implementada foi a utilização de swap tokens, que são uma espécie de passe para que os processos terminem sua execução e liberem espaço, evitando o trashing da memória.

Área de swap

Como ela é utilizada?
A área de swap é utilizada quando o kernel está sem memória devido a chamada de um fork para criar um processo filho, um brk para aumentar o segmento de dados, para alocar uma pilha que estourou o espaço alocado ou para trazer de volta um processo que já estava no disco.
Para determinar quem será guardado no disco, o swapper procura por arquivos que estão bloqueados, que estão há mais tempo parados e pela prioridade.
Após isso, o swapper ainda verifica de quando em quando se é possível recolocar um processo que está no disco de volta para a memória. Algumas vezes, é preciso fazer o swap de processos na memória para devolver um processo que estava no disco (o que é chamado de hard swap).
A área de swap é ativada principalmente para compensar o excesso de consumo de memória de alguns programas, algum eventual processo que ocupe muito mais memória do que o esperado, para otimizar o uso da memória desalocando processos da memória para dar lugar à memória-cache, e para hibernar o sistema.

Qual o tamanho ideal da área de swap?
O tamanho ideal para a área de swap é bastante discutido e o senso comum é de que o ideal é que esse tamanho esteja entre n e 2*n, onde n é o tamanho de sua memória física.
Note que o processo de hibernação guarda toda a ram no swap, portanto nesse caso o swap deve possuir no mínimo o tamanho da ram!

Referências
http://jno.glas.net/data/prog_books/lin_kern_2.6/0596005652/understandlk-CHP-17-SECT-4.html
http://www.cab.u-szeged.hu/local/linux/doc/sag/node63.html
https://help.ubuntu.com/community/SwapFaq
http://se9.blogspot.com/2008/02/design-issue-i-came-across-while.html
A.S.Tanbaum, Modern Operating System 2nd ed

Segurança

O Linux utiliza o conceito de usuários e grupos, cada um identificado por um ID de 16 bits. A associação de usuários com grupos é feita por um administrador do sistema (que pertence ao grupo de administradores). Quando um processo é criado, ele carrega o UID (ID do usuário) e GID (ID do grupo) do seu criador. Quando um arquivo é criado, ele carrega as UID e GID do processo que o criou.
Esse sistema é utilizado para determinar permissões para arquivos, que são representadas por 9 bits, que representam permissão de leitura, escrita e execução para o criador, grupo do criador e todos (representado por rwxrwxrwx na ordem).
Um mecanismo especial do Linux é ter o SETUID bit nos programas, que funciona da seguinte forma: se um usuário executa o programa, o UID do processo torna-se o UID do criador do programa, e não do usuário que executou o programa; dessa forma é possível criar permissões que se aplicam a programas, e não a usuários que executam os programas.

A seguir, vamos analisar alguns tópicos sobre a segurança do Linux: invasão de áreas de outros processos e de sistema, acesso à áreas não autorizadas e transferência de dados do Kernel para processos e vice-versa.

Um processo pode invadir áreas de outros processos?
Não. Cada processo possui seu próprio espaço de endereçamento, não permitindo que um processo invada a área de outros processos.
Os espaços de endereçamento dos processos são administrados pelo kernel, que controla o acesso à área de cada processo.

Um processo pode invadir áreas do sistema?
O acesso às áreas do sistema são controlados a partir de permissões. O sistema verifica as permissões do processo que requer acesso e, a partir do nível de permissão concedido ao processo, determina se o acesso é autorizado ou não.

O que acontece quando um processo tenta acessar uma área não autorizada?
O acesso à uma área não autorizada caracteriza uma exceção, que será tratada pelo kernel.
Por exemplo, ao tentar acessar uma área não autorizada, podemos gerar uma Bounds Check fault, que ocorre quando uma instrução é realizada com operandos fora das áreas de endereço válidos. Neste caso, o sinal gerado é um SIGSEGV.

Como ocorre a tranferência de dados do kernel para processos e vice-versa?
A presença de dados inconsistentes ou maliciosos no kernel pode trazer instabilidades e insegurança ao sistema. Por isto, é necessário verificar previamente os dados antes de transmití-los ao kernel. Isto ocorre através de uma série de funções pertencentes à API do kernel do linux.
A transferência de dados ocorre de acordo com a figura abaixo, realizando um armazenamento prévio em buffer, de modo a verificar a consistencia e validade dos dados. Uma vez verificado, os dados são transmitidos para o kernel.



Referências:
http://www.gelato.unsw.edu.au/~dsw/public-files/kernel-docs/kernel-api/index.html
http://tldp.org/HOWTO/Security-HOWTO/
http://www.ibm.com/developerworks/br/library/j-zerocopy/index.html
http://en.wikipedia.org/wiki/Security-Enhanced_Linux

Tratamento de Áreas de Memória Fixas

Existem páginas na memória que não podem ser desalocadas no processo de swap, sendo portanto áreas de memória fixas.
Isso é uma característica importante em programas de tempo-real, uma vez que ocasionais faltas de página podem denegrir a performance devido ao processo de recuperação de página que inclui a busca dos dados da página, a busca por um página vagável na memória física e a ocupação da página.
A interface para utilizar este mecanismo é o mlock() e munlock() que serão brevemente descritos abaixo (também existem os mlockall()/munlockall() e memcntl()).
É bom saber que a região da memória a ser fixada é quantizada em páginas, e por isso a os endereços de memória que serão fixados devem se referir a extremos de páginas. Além disso, há dois fatores que devem ser levados em consideração antes de se projetar a utilização de áreas de memória fixas: o processo deve ter os privilégios necessários para utilizar este artifício e a quantidade da área de memória fixada não pode extrapolar certos limites, pois o SO reserva uma área de memória que não pode ser fixada.
As funções mlock e munlock estão na biblioteca sys/mman.h e recebem os mesmos parâmetros:
1) Endereço: Endereço de início da área memória que deve ser fixada.
2) Comprimento: Tamanho da área de memória que será fixada.

É interessante citar que as funções que terminam com all fazem o tratamento de todo o espaço de endereço do processo.

Referências
http://blogs.sun.com/thejel/entry/locking_memory
http://linux.die.net/man/2/mlock

Mapeamento de Arquivos na Memória Virtual

O mapeamento de arquivos na memória virtual consiste no mapeamento de uma seção do arquivo (o que pode ser o arquivo inteiro) na memória e criação de ponteiros para esta área na memória. Deste modo, é possível ter acesso ao conteúdo do arquivo através de ponteiros.
Uma utilização trivial deste tipo de mecanismo é o próprio carregamento dos processos na memória, quando o SO pega o arquivo com o programa e carrega na área de texto.
Isto também é muito utilizado para compartilhamento entre processos. Quando dois processos acessam o mesmo arquivo, é preciso "reabrir" o arquivo a todo o momento, o mapeamento na memória evita esse tipo de problema, mas é essencial que os processos utilizem semáforos para evitar problemas de concorrência.
A chamada de sistema que realiza esta função é o mmap(...) que deve receber um arquivo que possui um descritor gerado com a chamada de sistema fopen(...).
Um exemplo de utilização é:
#include <unistd.h>
#include <sys/types.h>
#include <sys/mman.h>

int descritor_de_arquivo, tamanho_da_pagina;
char *dados;

descritor_de_arquivo = fopen("arquivo", O_RDONLY);
tamanho_da_pagina = getpagesize();
dados = mmap((caddr_t)0, 10*tamanho_da_pagina, PROT_READ, MAP_SHARED, descritor_de_arquivo, 0);

Os parâmetros da função mmap são:
1) Endereço: Ao utilizar (caddr_t)0, por padrão o sistema atribui para um endereço que é retornado na chamada da função (caso contrário, a variável dados poderia receber o endereço pré-determinado e apontaria para a mesma região).
2) Tamanho: É a quantidade de dados que queremos mapear (preferencialmente utiliza-se um múltiplo do tamanho da página caso contrário será mapeada uma quantidade de 0's que complete o tamanho da página e que não será persistida no disco).
3) Proteção: Recebe os valores (PROT_READ|PROT_WRITE|PROT_EXEC) que podem ser separados por '|' caso sejam permitidos mais de um tipo de permissão de acesso. É importante ressaltar que a permissão do mapeamento deve ser coerente com a permissão encontrada no descritor do arquivo.
4) Flags: Há diversos tipos de flags que podem ser setados, mas sempre um de MAP_SHARED e MAP_PRIVATE deve ser citado (o primeiro é para mapeamentos compartilhados entre processos e o segundo para mapeamentos privados), outros valores para isso podem ser encontrados com uma descrição mais detalhada no site http://linux.die.net/man/2/mmap
5) Descritor de Arquivo: O descritor do arquivo que será mapeado e que deve ter sido inicializado anteriormente.
6) Endereço do Arquivo Inicial: Indica onde o mapeamento do arquivo deve iniciar, uma vez que é possível mapear apenas a parte desejada do arquivo.

Para desmapear o arquivo, pode-se terminar o processo ou utilizar a chamda munmap(...), no entanto, é interessante notar que acabar com o descritor não irá desmapear o arquivo.
A chamada ummap pode desmapear apenas parte do arquivo, veja os parâmetros que são passados:
1) Endereço: Pode ser o endereço recebido da função mmap ou esse valor acrescido do endereço no arquivo inicial do desmapeamento.
2) Comprimento: É o tamanho do bloco que será desmapeado.

Isto resume o mapeamento de memória no linux utilizado com foco no compartilhamento de dados entre processos.

Referências
http://www.ecst.csuchico.edu/~beej/guide/ipc/mmap.html
http://en.wikipedia.org/wiki/Memory-mapped_file
http://linux.die.net/man/2/mmap

domingo, 12 de julho de 2009

Mapeamento de Arquivos na Memória Virtual

Referências:
http://en.wikipedia.org/wiki/Virtual_memory

Compartilhamento de Memória

A memória compartilhada consiste de uma região de memória que pode ser acessada por dois ou mais processos.
Esta área comum aos processos é utilizada por cada um deles como pertencente à sua área de memória e é utilizada também como um modo eficiente de realizarmos troca de dados entre os
processos.
Como todos os processos podem alterar esta área de memória, certos cuidados devem ser tomados. Como por exemplo, não permitindo acesso simultâneo à esta área compartilhada através do uso de semáforos.

O compartilhamento de memória envolve a criação de um segmento de memória compartilhada, acoplamento e desacoplamento ao segmento de memória compartilhada e as operações de ler e escrever (acesso à memória compartilhada).

A criação do segmento é realizada através da chamada shmget(), que possui o seguinte protótipo:
 int shmget(key_t key, size_t size, int shmflg);
Nesta criação, define-se as permissões de acesso e o tamanho de bytes do segmento. O retorno desta função é um inteiro, responsável por identificar o segmento de memória compartilhado. Este identificador deve ser utilizado pelos processos que desejam ler ou escrever nesta memória compartilhada.
Antes de ler ou escrever, precisamos acoplar o processo ao segmento de memória compartilhada, através da função shmat(), passando o identificador do segmento como parâmetro. A função possui o seguinte protótipo:
void *shmat(int shmid, void *shmaddr, int shmflg);


A função retorna um ponteiro, que é utilizado para desacoplar o processo do segmento compartilhado.
Para desacoplar o processo do segmento de memória compartilhada, utilizamos a função, passando como parâmetro o ponteiro de retorno de shmat. A função possui o seguinte protótipo:

 int shmdt(void *shmaddr);

Para maiores informações e um exemplo de uso destas funções descritas acima, consulte as referências.

Referências:
http://www.ecst.csuchico.edu/~beej/guide/ipc/shmem.html