21
ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO Departamento de Engenharia de Computação e Sistemas Digitais PCS2042 Sistemas Operacionais Projeto2 2 Visão geral de processos no Minix Grupo: 8 Os sinais e o Minix. Professor: Jorge Kinoshita Equipe: Oduvaldo Vick Pedro d'Aquino F. F. de Sá Barbuda Pedro Monteiro Kayatt

PCS2042 Sistemas Operacionais - USPjkinoshi/2008/projs/r2-pedrodaquino...PCS2042 – Sistemas Operacionais Projeto 22 Visão geral de processos no Minix Grupo: 8 – Os sinais e o

  • Upload
    others

  • View
    1

  • Download
    0

Embed Size (px)

Citation preview

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

PCS2042 – Sistemas Operacionais

Projeto222

Visão geral de processos no Minix

Grupo: 8 – Os sinais e o Minix.

Professor: Jorge Kinoshita

Equipe:

Oduvaldo Vick

Pedro d'Aquino F. F. de Sá Barbuda

Pedro Monteiro Kayatt

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

Os Objetivos

O objetivo desta segunda fase do projeto é obter uma visão geral de como os processos interagem com o

sistema operacional Minix.

Neste relatório estão descritos os passos tomados pelo grupo oito para um esclarecimento no processo de

chamada de Sinais, demonstrando como podemos capturá-los e tratá-los.

Enunciado: onde está o código do minix que trata dos sinais (signals)? Caso um processo não tenha

decladrado o signal handler para tratar SIGINT (control C) qual é o código executado pelo minix?

dica: signal.c, SIG_DFL.

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

O que são sinais?

Sinais podem ser descritos como um mecanismo para transmitir informações para um processo que não

está necessariamente esperando por uma entrada de dados. Desta forma podemos fazer um paralelo

destes com as interrupções de hardware; pois eles interrompem um processo que estava rodando

(funcionando, então, de forma assíncrona); exigem o desvio para uma área de código especial; e o processo

que foi interrompido continua sua operação normal, através da persistência do seu estado anterior à

chamada do sinal.

A geração dos sinais se origina de diversas maneiras, entre estas podemos citar:

Pelo sistema: através de combinações de teclas (ex. Ctrl+C)

Por outros processos: através de códigos de manipulação de sinais de chamadas de sistema (ex.

system call “kill”)

Através de funções de sistema qualquer processo pode escolher efetuar três tipos de operações com os

sinais: ignorar, capturar ou executar a chamada default.

Outra grande aplicação dos sinais são nas funções de temporizadores, sendo utilizados então para o

desenvolvimento de alarmes, extremamente úteis para a administração do sistema operacional.

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

A Metodologia

Para o desenvolvimento desse projeto estudamos o capitulo 4.7.7 e 4.8.6 do livro Sistemas Operacionais –

Projeto e Implementação 1do Tanenbaum. Nessa seção do livro é descrito passo a passo o que são sinais e

como é feita a abordagem destes pelo código do Minix.

Para confirmar o que é dito neste capítulo elaboramos um pequeno programa, que capturava um sinal e

percorria a pilha, procurando pelas estruturas que o livro dizia estarem lá. O código do programa segue na

seção “Do ponto de vista do Sistema”. O objetivo deste teste é verificar se o Minix realmente aloca 100

bytes de estruturas (sigcontext + sigframe) na pilha.

1 SISTEMAS OPERACIONAIS, PROJETO E IMPLEMENTAÇÃO - 3ª EDIÇÃO

TANENBAUM & WOODHULL

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

Como os sinais funcionam?

Para entender o funcionamento dos sinais resolvemos dividir esta abordagem em duas:

O ponto de vista do Usuário

O ponto de vista do Sistema

Ambas as abordagens são descritas a seguir:

O ponto de vista do Usuário: Do ponto de vista do usuário os sinais são extremamente simples, um fato bom, porém preocupante, pois

muitos dos desenvolvedores não sabem ao certo como estes estão funcionando no sistema.

Para o programador utilizar da manipulação de sinais ele pode simplesmente capturá-lo, e para tal basta

utilizar de uma chamada definida no sistema chamada “sigaction”. Outra estrutura importante é a

“sigprocmask” que auxilia no bloqueio de sinais.

A seguir vemos como é definida a chamada da sigaction:

sigaction(int numeroSinal, struct sigaction* psaction,

struct sigaction* pVelhoSaction)

Como argumentos desta chamada temos:

int numeroSinal: corresponde a um tipo de sinal que queira ser gerenciado. Estas declarações

são feitas em signal.h que pode ser visto a seguir:

/* Regular signals. */

#define SIGHUP 1 /* hangup */

#define SIGINT 2 /* interrupt (DEL) */

#define SIGQUIT 3 /* quit (ASCII FS) */

#define SIGILL 4 /* illegal instruction */

#define SIGTRAP 5 /* trace trap (not reset when caught) */

#define SIGABRT 6 /* IOT instruction */

#define SIGBUS 7 /* bus error */

#define SIGFPE 8 /* floating point exception */

#define SIGKILL 9 /* kill (cannot be caught or ignored) */

#define SIGUSR1 10 /* user defined signal # 1 */

#define SIGSEGV 11 /* segmentation violation */

#define SIGUSR2 12 /* user defined signal # 2 */

#define SIGPIPE 13 /* write on a pipe with no one to read it */

#define SIGALRM 14 /* alarm clock */

#define SIGTERM 15 /* software termination signal from kill */

#define SIGEMT 16 /* EMT instruction */

#define SIGCHLD 17 /* child process terminated or stopped */

#define SIGWINCH 21 /* window size has changed */

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

/* POSIX requires the following signals to be defined, even if they are

* not supported. Here are the definitions, but they are not supported.

*/

#define SIGCONT 18 /* continue if stopped */

#define SIGSTOP 19 /* stop signal */

#define SIGTSTP 20 /* interactive stop signal */

#define SIGTTIN 22 /* background process wants to read */

#define SIGTTOU 23 /* background process wants to write */

#define _NSIG 23 /* number of signals used */

struct sigaction* psaction: corresponde a um ponteiro para uma estrutura que irá definir a

função de tratamento do sinal. (define que a nova ação do sinal será copiada no espaço de PM).

struct sigaction* pVelhoSaction: corresponde a um ponteiro para uma estrutura que

armazena atributos de sinais antigos.

Notoriamente ambas as estruturas pode apontar pra valores pré-definidos; estes são:

SIG_DFL – aponta para o tratamento “default” (padrão), do sinal. Se este o tem.

SIG_IGN – faz com que o sinal seja ignorado pelo processo.

Definidos, junto com outras macros, signal.h:

/* Macros used as function pointers. */

#define SIG_ERR ((__sighandler_t) -1) /* error return */

#define SIG_DFL ((__sighandler_t) 0) /* default signal handling */

#define SIG_IGN ((__sighandler_t) 1) /* ignore signal */

#define SIG_HOLD ((__sighandler_t) 2) /* block signal */

#define SIG_CATCH ((__sighandler_t) 3) /* catch signal */

#define SIG_MESS ((__sighandler_t) 4) /* pass as message (MINIX) */

OBS: sigkill não pode ser ignorado nem capturado

E agora podemos ver como é declarada a chamada do sigprocmask:

sigprocmask(int how, sigset_t *set, sigset_* oset)

E temos como argumentos: o inteiro how, que utiliza os valores pré-definidos em signal.h exibidos a seguir;

e os ponteiros para as estruturas set e oset, que de forma semelhante a sigaction definem atributos atuais

e antigos.

/* POSIX requires these values for use with sigprocmask(2). */

#define SIG_BLOCK 0 /* for blocking signals */

#define SIG_UNBLOCK 1 /* for unblocking signals */

#define SIG_SETMASK 2 /* for setting the signal mask */

#define SIG_INQUIRE 4 /* for internal use only */

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

E então podemos ver como é a montagem de uma estrutura do tipo sigaction:

struct sigaction{

void (*sa_handler)(int sig); /*SIG_DFL, SIG_IGN, SIG_MESS, ou o

ponteiro para função */

sigset_t sa_mask; /*sinais a serem bloqueados durante a execução da

rotina de tratamento*/

int sa_flags; /*flags especiais*/

}

Assim podemos entender o quanto é simplista a elaboração de uma rotina e a definição para que esta

possa manipular o recebimento de um sinal para o processo. A seguir exemplificamos um pseudo-código

que efetua a manipulação do SIGINT (sinal gerado pelo sistema ao pressionar Ctrl+C no Shell).

void rotinaTratamento(int qualSinal) { /*Declaração da rotina para tratar o

sinal*/

printf(“Rotina de tratamento do SIGINT”);

}

int main(){

struct sigaction saction; /*cria-se a estrutura para manipular o sinal*/

saction.handler = rotinaTratamento; /*fazemos com que esta aponte para a

função rotinaTratamento */

sigaction(SIGINT, &saction, NULL); /*Especificamos que o sinal SIGINT deve

ser tratado pela nossa estrutura saction*/

pause();

printf(“Rotina de tratamento terminou”);

}

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

O ponto de vista do sistema Para o usuário, a utilização dos sinais é bastante simples. Para o sistema operacional, entretanto,

há alguma complicação no processo – especialmente no caso em que o sinal é capturado (quando é

ignorado ou quando a ação padrão deve ser executada, a implementação é trivial). De agora em diante,

vamos nos referir a “sinais capturados” simplesmente como “sinais”.

Do ponto de vista do sistema, os sinais são como uma interrupção de alto nível. Há diversas

semelhanças:

Um processo estava rodando e foi interrompido

Há uma rotina que deve chamada para tratar da interrupção

O processo que estava executando não deve perceber que a sua execução foi interrompida

(o que implica na exigência de que o contexto se mantenha igual)

Há, porém, uma restrição a mais no caso dos sinais: a rotina de tratamento pode ser escrita como

uma função normal em C – já rotinas de tratamento de interrupção necessariamente envolvem

componentes de baixo nível, como instruções especiais etc.

Isso é um retrato da diferença fundamental entre sinais e interrupções: enquanto interrupções são

lidadas pelo processador e pelo núcleo (até em sistemas de microkernel), os sinais são definidos num nível

muito mais abstrato. Isso permite que a sofisticação do sistema seja maior: o tratamento de interrupções

vai ser sempre difícil e cru, pois estamos muito perto do hardware; os sinais, contudo, são um conceito

muito mais distante do processador.

Porém, isso não significa que a implementação dos sinais não seja desafiadora. Além de salvar o

contexto do processo, ainda precisamos criar uma pilha adequada para que a rotina de tratamento possa

rodar – e, depois disso, precisamos restaurar o estado dos registradores. E, finalmente, isso tudo deve ser

feito de maneira transparente para o programador tanto do processo, quanto da rotina de interrupção.

Salvando o contexto Assim que o Process Manager percebe que um sinal deve ser enviado para um processo, e que o sinal foi

capturado, ele inova a chamada de núcleo “sys_sigsend”. É nela, definida no arquivo

/kernel/system/do_sigsend.c, que a parte suja do trabalho está concentrada.

O primeiro passo é salvar o contexto do processo interrompido. Isso é feito nas seguintes linhas:

/* Compute the user stack pointer where sigcontext will be stored. */

scp = (struct sigcontext *) smsg.sm_stkptr - 1;

/* Copy the registers to the sigcontext structure. */ memcpy(&sc.sc_regs, (char *) &rp->p_reg, sizeof(struct sigregs)); /* Finish the sigcontext initialization. */ sc.sc_flags = SC_SIGCONTEXT;

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

sc.sc_mask = smsg.sm_mask; /* Copy the sigcontext structure to the user's stack. */ dst_phys = umap_local(rp, D, (vir_bytes) scp, (vir_bytes) sizeof(struct sigcontext)); if (dst_phys == 0) return(EFAULT); phys_copy(vir2phys(&sc), dst_phys, (phys_bytes) sizeof(struct sigcontext));

O que o código acima faz é copiar os registradores do processo, que estão armazenados na tabela do

núcleo, na pilha do processo. Essa é, aliás, uma diferença importante entre o tratamento de interrupção e

de sinais. No primeiro caso, o contexto do processo interrompido é guardado na tabela de processos. Isso

elimina a possibilidade de interrupções aninhadas (o contexto salvo seria reescrito) – de fato, o núcleo do

Minix não é reentrante. Por outro lado, quando um sinal é recebido por um processo, seu contexto é salvo

na sua própria pilha – e isso permite sinais aninhados. A estrutura sigcontext contém os seguintes valores:

struct sigcontext { int sc_flags; /* sigstack state to restore */ long sc_mask; /* signal mask to restore */ struct sigregs sc_regs; /* register set to restore */ };

A estrutura sigregs contém os registradores do processador, e algumas informações a mais. Para a

arquitetura x86, ela é como segue:

struct sigregs { short sr_es; short sr_ds; int sr_di; int sr_si; int sr_bp; int sr_st; /* stack top -- used in kernel */ int sr_bx; int sr_dx; int sr_cx; int sr_retreg; int sr_retadr; /* return address to caller of save -- used * in kernel */ int sr_pc; int sr_cs; int sr_psw; int sr_sp; int sr_ss; };

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

Criando a pilha Agora que o contexto do processo já foi salvo na sua própria pilha, o sistema operacional deve cuidar de

criar uma pilha adequada para a rotina de tratamento do sinal. Isso é necessário pois a rotina de

tratamento é escrita em C, e, portanto, o compilador espera que uma pilha adequada aos padrões da

linguagem esteja formada quando a função for invocada.

Mas o que significa “uma pilha adequada”? Basicamente, uma pilha que respeite as convenções de

chamada da linguagem e do processador (no nosso caso, x86 e C, respectivamente). A convenção de

chamada da arquitetura x86 é colocar o endereço de retorno na pilha do processo. Isso é feito

automaticamente pelo processador, quando ele encontra uma instrução “call” – e a instrução “ret” assume

que o endereço de retorno também esta na pilha.

A convenção da linguagem diz respeito à passagem dos parâmetros e à forma de retornar valores. No caso

da linguagem C (chamada cdecl), a passagem dos parâmetros é feita na pilha, da direita para a esquerda; e

o valor retornado por uma função está sempre no registrador EAX.

Um curto exemplo:

Figura 1 - Exemplo da convenção de chamada do C/x86

No Minix, todas as rotinas de tratamento de sinal precisam ser do mesmo tipo, __sighandler_t. Esse

tipo está definido no arquivo de cabeçalho signal.h:

/* The sighandler_t type is not allowed unless _POSIX_SOURCE is defined. */ typedef void _PROTOTYPE( (*__sighandler_t), (int) );

Isso quer dizer que todas as funções devem ser do tipo: void nomeRotina(int qualSinal).

Realmente, não faz sentido uma rotina de tratamento retornar algum valor (para quem ela retornaria?). O

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

Minix só precisa se preocupar, portanto, com o endereço de retorno e com a passagem do parâmetro

inteiro, que diz qual foi o sinal interrompido.

Para conseguir criar esse ambiente, o Minix usa uma outra estrutura, chamada sigframe:

struct sigframe { /* stack frame created for signalled process */ void (*sf_retadr) (void); int sf_signo; int sf_code; struct sigcontext *sf_scp; int sf_fp; void (*sf_retadr2)(void); struct sigcontext *sf_scpcopy; };

Essa estrutura é colocada na pilha do usuário, logo acima de sigcontext. Ela é empilhada de tal forma que o

primeiro campo, sf_retadr (um ponteiro para uma função) fique no topo da pilha. Esse ponteiro é na

verdade o endereço de retorno da função de tratamento. Logo abaixo vem o campo signo, que contém o

número do sinal que está sendo tratado. Como esse valor está logo acima do endereço de retorno, a pilha

já está formada: o compilador vai conseguir acessar o parâmetro qualSinal, e o processador vai conseguir

efetuar a instrução ret.

O trecho de código a seguir, da função do_sigsend do kernel, inicia e copia a estrutura sigframe na pilha do

processo:

/* Initialize the sigframe structure. */ frp = (struct sigframe *) scp - 1; fr.sf_scpcopy = scp; fr.sf_retadr2= (void (*)()) rp->p_reg.pc; fr.sf_fp = rp->p_reg.fp; rp->p_reg.fp = (reg_t) &frp->sf_fp; fr.sf_scp = scp; fr.sf_code = 0; /* XXX - should be used for type of FP exception */ fr.sf_signo = smsg.sm_signo; fr.sf_retadr = (void (*)()) smsg.sm_sigreturn; /* Copy the sigframe structure to the user's stack. */ dst_phys = umap_local(rp, D, (vir_bytes) frp, (vir_bytes) sizeof(struct sigframe)); if (dst_phys == 0) return(EFAULT); phys_copy(vir2phys(&fr), dst_phys, (phys_bytes) sizeof(struct sigframe));

Após essas duas estruturas terem sido copiadas pelo kernel , o estado da pilha é o seguinte:

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

Figura 2 - Pilha do processo interrompido por um sinal

Figura 3 - Estrutura sigframe; sf_scp e sf_scpcopy apontam para sigcontext

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

Desviando a execução Agora que a pilha já está preparada e o contexto, salvo, é só desviar o fluxo para a rotina de tratamento.

Isso é feito da seguinte forma:

/* Reset user registers to execute the signal handler. */ rp->p_reg.sp = (reg_t) frp; rp->p_reg.pc = (reg_t) smsg.sm_sighandler;

A primeira linha muda o registrador de pilha para apontar para a nova pilha, e a segunda modifica o PC

(registrador EIP na arquitetura x86) para que ele execute a rotina de tratamento especificada pelo usuário.

Desse modo, na próxima vez que o processo for escalonado, o kernel irá copiar esses registradores da

tabela de processos (onde eles foram salvos), e a execução continuará na rotina de tratamento.

Retornando da rotina Após a rotina de tratamento executar, ela efetuará a instrução “ret”, que faz o processador executar o

código apontado pelo endereço de retorno, que estava na pilha. A rotina não pode voltar para o ponto em

que o processo estava quando foi interrompido, pois o contexto ainda não foi restaurado (e a pilha está

distorcida). Nós precisamos efetuar um trabalho de limpeza antes de voltarmos à execução normal do

processo. Esse trabalho de limpeza é feito por uma função chamada do_sigreturn, do kernel. Para que o

processo chegue até ela, o Minix primeiro coloca como endereço de retorno uma função em modo usuário

chamada __sigreturn:

fr.sf_retadr = (void(*())smsg.sm_sigreturn;/* sm_sigreturn aponta __sigreturn */

Ela é uma curta função escrita em assembly, de apenas duas linhas (note que ela está declarada com três

underlines ao invés de 2; isso acontece porque a convenção do compilador é de que funções em C tenham

um “_” antes do nome quando referenciadas no código assembly; por esse mesmo motivo _sigreturn()

aparece no código abaixo com dois underlines):

___sigreturn: add esp, 16 jmp __sigreturn

Quando __sigreturn é executada, o processador já desempilhou o endereço de retorno, e o registrador de

pilha, esp, agora aponta para o parâmetro da rotina (o número do sinal). A primeira coisa que __sigreturn

faz é somar 16 à pilha, o que faz esp apontar para sf_retadr2. Isso significa que boa parte da estrutura

sigframe é completamente inútil – não é usada em instante algum pelo Minix. Logo depois, __sigreturn

pula para a rotina _sigreturn(), escrita em C. Seu protótipo é:

int _sigreturn(struct sigcontext* scp)

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

Note que _sigreturn recebe um parâmetro – o ponteiro para a estrutura sigcontext, que contém os

registradores. Para que o compilador consiga acessar esse endereço, ele deve estar duas posições acima na

pilha (a posição superior é do endereço de retorno) – e é exatamente essa a situação que temos:

Figura 4 - A pilha quando _sigreturn é chamada

A variável sf_retadr2 tem, portanto, o único efeito de enganar o compilador - _sigreturn() nunca retornará

normalmente, através de uma instrução ret, porque ela irá restaurar os registradores salvos do processo e,

portanto, o endereço de retorno que está na pilha é completamente indiferente. Com a adição dela,

porém, o compilador consegue acessar a variável sf_scpcopy, que é passada como parâmetro para

_sigreturn().

A função _sigreturn(), que ainda está em modo usuário, nada mais faz além da chamada de núcleo

“sys_sigreturn”. Porém, ela contém uma particularidade interessante: como ela está executando numa

pilha que será alterada muito em breve pelo núcleo, ela não pode ter variáveis locais (que sempre ficam na

pilha:

/* The message can't be on the stack, because the stack will vanish out * from under us. The send part of sendrec will succeed, but when * a message is sent to restart the current process, who knows what will * be in the place formerly occupied by the message? */ static message m;

Fazendo a variável “m” ser static, o Minix garante que ela será alocada numa região diferente da memória

(o bss).

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

A rotina que realmente restaura o contexto é a do_sigreturn, definida num arquivo de mesmo nome na

pasta /kernel/system. Sua única particularidade é o seguinte trecho:

/* Don't panic kernel if user gave bad selectors. */ sc.sc_cs = rp->p_reg.cs; sc.sc_ds = rp->p_reg.ds; sc.sc_es = rp->p_reg.es;

Aqui, o Minix está se protegendo contra seletores de segmentos adulterados, o que poderia causar uma

falha no kernel – o que derrubaria o sistema. Isso é uma medida interessante de robustez – os dados que

vem do modo usuário nunca podem ser confiados cegamente.

Por fim, a linha em que o Minix de fato restaura os registradores (isto é, restaura a sua cópia dos

registradores que está na tabela de processos; eles só serão de fato restaurados quando o processo for

escalonado para rodar):

/* Restore the registers. */ memcpy(&rp->p_reg, &sc.sc_regs, sizeof(struct sigregs));

Verificando a pilha Para confirmar nosso entendimento de como o Minix fabrica a pilha para que a rotina de tratamento rode,

o grupo elaborou um pequeno programa que tenta atravessá-la, andando um número fixo de bytes, para

encontrar uma variável local na função que foi interrompida. O código segue abaixo:

#define _POSIX_SOURCE 1 #include <stdlib.h> #include <stdio.h> #include <signal.h> void callback(int); void espera(void); int main(void){ struct sigaction sa; /* declara a estrutura de ação */ sa.sa_handler = callback; /* define a função que será chamada */ sigaction(SIGINT, &sa, NULL); /* registra */ espera(); /* espera indefinidamente */ } /* Esta é a função que será interrompida pelo SIGINT */ void espera(void){ int magico = 0x12345678; /* "magico" é a única variável local desta função, e, portanto, estará logo acima do sigcontext na pilha */ while(1){ ; /* espera eternamente pelo sinal */ }

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

} /* callback é a função de tratamento do sinal SIGINT */ void callback(int which){ int magico; /* o numero magico; deve ser 0x12345678 */ char* ptr = (char*) &which; /* começamos apontando para o parâmetro */ ptr += 96; /* subimos 96 bytes na pilha ... */ magico = *((int*)ptr); /* ... o que nos faz chegar até o número mágico */ printf("O numero magico e' %x\n", magico); exit(0); }

Nós precisamos avançar 96 bytes na pilha pois as duas estruturas, sigcontext e sigframe, têm somadas 100

bytes. Contudo, nós começamos a percorrer a partir do parâmetro “which”, que já está acima do endereço

de retorno – que ocupa 4 bytes.

De fato, o programa funcionou, imprimindo a seguinte mensagem:

Figura 5 - Resultado do programa de teste

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

Alarmes e Temporizadores

Usaremos os sinais de alarme para exemplificar o uso de sinais no Minix 3, visto que é um dos usos

mais comuns de sinais em um sistema operacional.

O que são?

Alarme é um sinal utilizado para despertar um processo após um período de tempo pré-definido.

Pode ser usado, por exemplo, para fechar um processo que está há muito tempo esperando por um evento

que não acontece.

No Minix 3 o sinal de alarme recebe o nome de SIGALRM, e seu tratamento default é matar o

processo que recebe o sinal.

Alarmes no Minix 3 x Alarmes em sistemas monolíticos

Em um sistema monolítico convencional os alarmes são gerenciados inteiramente pelo núcleo,

enquanto no Minix 3 (sistema microkernel) os alarmes de processos de sistema são gerenciados pelo

núcleo e os alarmes de processos de usuário são gerenciados pelo Gerenciador de Processos.

O objetivo dessa separação do tratamento de alarmes no Minix 3 é diminuir a carga de código do

núcleo visando obter o microkernel, dessa maneira diminui-se a quantidade de erros em espaço de núcleo,

que são considerados mais sérios do que erros em espaço de usuário.

No entanto, não é possível gerenciar os alarmes de processos de usuário sem usar código em

espaço de núcleo, porque primeiramente os alarmes precisam ser gerenciados também pela Tarefa de

Relógio (presente no núcleo), que mantém uma lista encadeada de temporizadores. Toda vez que ocorre

uma interrupção de relógio compara-se o tempo corrente com o tempo de expiração do temporizador no

início da fila, e caso o tempo já tenha expirado é iniciado o processo de emissão do sinal de Alarme e

notificações são feitas pela Tarefa de Relógio para o processo que solicitou o alarme.

Em sistemas monolíticos a Tarefa de Relógio é responsável pelo controles dos temporizadores de

todos os processos, ou seja, a única fila de temporizadores existente no núcleo que é utilizada pela Tarefa

de Relógio. Já no Minix 3 existem duas filas de temporizadores, uma no núcleo que possui temporizadores

de processos de sistema e temporizadores do Gerenciador de Processos (único processo rodando em modo

usuário que possui temporizadores na fila do núcleo), e outra no próprio Gerenciador de Processos, que

mantém temporizadores dos processos de usuário.

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

Funcionamento de Alarmes no Minix 3

Nessa seção mostraremos a seqüência de eventos de um sinal SIGALRM, que utiliza as duas filas de

temporizadores citadas, em um processo que possui uma rotina de tratamento para esse sinal.

Figura mostrando seqüência de eventos:

Passos :

1: Um processo de usuário faz uma chamada de alarme para o Gerenciador de Processos (PM)

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

2: O Gerenciador de Processos configura um novo temporizador (função set_alarm) para o

processo que chamou o alarme em sua fila e faz uma confirmação para o mesmo.

O temporizador configurado possui um ponteiro para uma função cão de guarda chamada

cause_sigalarm, essa função será chamada no momento em que o temporizador expirar, para iniciar o

processo de envio de alarme.

Nada acontece enquanto esse temporizador não chegar ao início da fila do Gerenciador de

Processos (os outros temporizadores que estão na frente saem por expiração ou por cancelamento).

3: Quando o temporizador configurado chegar ao início da fila, o Gerenciador de Processos enviará

uma mensagem à Tarefa de Sistema (System) no núcleo para que essa configure um novo temporizador

para ele (Gerenciador de Processos) na fila do núcleo (usada pela Tarefa de Relógio).

O temporizador configurado para o Gerenciador de Processos também possui um ponteiro para

uma função cão de guarda chamada cause_alarm, essa função será chamada no momento em que o

temporizador expirar, para iniciar o processo de envio de alarme.

4: A Tarefa de Sistema confirma para o Gerenciador de Processos que um temporizdor foi

configurado para ele.

Novamente, nada acontece enquanto esse temporizador não chegar ao início da fila do núcleo.

5: Quando o temporizador configurado chegar ao início da fila do núcleo, a rotina de tratamento de

relógio passará a comparar o tempo de expiração do temporizador com o tempo corrente a cada

interrupção de relógio, quando for verificado que o tempo expirou a rotina de tratamento de relógio

enviará uma mensagem para a Tarefa de Relógio (Clock).

6: Nesse momento a função cão de guarda cause_alarm (criada para o temporizador do

Gerenciador de Processos) é acionada e notifica o Gerenciador de Processos que seu temporizador expirou

(lembrando que em sistema monolíticos essa mensagem seria enviada diretamente para o processo de

usuário que solicitou o alarme).

7: No Gerenciador de Processos a função cão de guarda cause_sigalarm (criada para o processo de

usuário que solicitou o alarme) é acionada.

Cause_sigalarm resulta na chamada de sig_proc, que verifica na estrutura sigaction do processo

que irá receber o alarme se ele possui uma rotina de tratamento ou se será executado o tratamento default

(matar o processo).

No caso do exemplo o processo possui uma rotina de tratamento para SIGALRM, então o

Gerenciador de Processos envia uma mensagem à Tarefa de Sistema.

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

8: Ela então realizará toda a manipulação de pilha (descrita nas seções anteriores) para o envio do

sinal de alarme para o processo usuário.

Logo em seguida a Tarefa de Sistema envia uma confirmação para o Gerenciador de Processos.

9: A Rotina de Tratamento (Handler) é executada, após isso sigreturn é chamado para iniciar a

recuperação do contexto que o processo possuía antes da chegada do alarme (para explicação mais

aprofundada ver seções anteriores).

10: Chamada de sigreturn chega ao Gerenciador de Processos e esse informa a Tarefa de Sistema.

11: Tarefa de Sistema faz a limpeza da pilha (recuperação de contexto) e confirma enviando uma

mensagem ao Gerenciador de Processos.

Vantagens e Desvantagens

Essa seção citará as principais vantagens de desvantagens de parte do gerenciamento de alarmes

pertencer ao Gerenciador de Processos e não ao núcleo.

Desvantagem:

Como pôde ser obervado no diagrama que mostra a seqüência de eventos do processo de envio de

SIGALRM existe muita transição entre a região de usuário e a região de núcleo do Sistema Operacional,

principalmente se levarmos em conta que para trocar um processo em execução é necessária uma

passagem pelo núcleo (por exemplo quando deixamos de executar o processo de usuário que requisita o

alarme para começar a executar o Gerenciador de Processos). E essa transição constante pode ser um

problema para o Sistema Operacional.

Vantagens:

Apesar de não parecer existir uma economia muito grande de execução por parte do núcleo no

Minix 3 em relação aos sistemas monolíticos, existem algumas situações onde essa economia fica evidente:

1: Caso onde mais de um temporizador expira no mesmo tique de relógio.

É improvável 2 processos requisitem alarmes ao mesmo tempo, no entanto a situação 1 ocorre

no caso de interrupções serem desabilitadas por um determinado tempo (por exemplo, na chamada para a

BIOS), porque os tiques de relógio desse período ficarão acumulados, e quando as interrupções forem

novamente habilitadas mais de um temporizador pode expirar ao mesmo tempo. O núcleo então terá

menos trabalho, pois só precisará notificar uma vez o Gerenciador de Processos, e esse que terá todo o

ESCOLA POLITÉCNICA DA UNIVERSIDADE DE SÃO PAULO

Departamento de Engenharia de Computação e Sistemas Digitais

trabalho de percorrer sua lista, limpando-a e gerando notificações para os processos de usuário (em um

sistema monolítico esse seria trabalho do núcleo).

2: Caso onde o temporizador é cancelado antes de chegar ao início da fila do Gerenciado de

Processos.

Um processo que solicitou alarme pode cancelar o mesmo antes desse expirar. Isso ocorre, por

exemplo, no caso de um processo que precisa esperar um certo evento ocorrer, mas que não quer esperar

esse evento para sempre e por isso cria o alarme. No entanto, no caso desse evento ocorrer antes do

tempo de expiração o alarme não é mais necessário, e então é cancelado. Caso o temporizador não tenha

chegado ao início da fila do Gerenciador de Processos o núcleo nem chegou a ser notificado da existência

desse alarme (ele só armazena dados do processo que está no início da fila do Gerenciador de Processos),

então para fazer o cancelamento basta excluir o temporizador da fila do Gerenciador de Processos, sem

nenhum trabalho para o núcleo. Em sistemas monolíticos como o gerenciamento de alarmes é todo do

núcleo, este teria o trabalho de fazer o cancelamento.