Você está na página 1de 8

UNIVERSIDADE FEDERAL DE SERGIPE

CENTRO DE CIÊNCIAS EXATAS E TECNOLOGIA


DEPARTAMENTO DE COMPUTAÇÃO

IDENTIFICAÇÃO
DISCIPLINA: SISTEMAS OPERACIONAIS CÓDIGO:
PROFESSOR: RENÊ GUSMÃO PERÍODO:

Atividade 3: Processos e threads

1. Criando um processo com a função da API do UNIX.

Para criar um novo processo a partir do processo em execução (de maneira semelhante ao início do
shell script a partir da execução do shell script), o programador UNIX pode usar a função fork() da
linguagem C disponível na API UNIX/Linux definida pelo padrão POSIX. A implementação típica
da criação de um novo processo com essa função é assim:

#include <sys/types.h> /* Important header file with some types */


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

int main()
{
pid_t justPid, myPid, myParentPid; /* pid_t is defined in types.h */

/* Fork new process: */


justPid = fork(); /* New process is created at this moment */

if ( justPid < 0 ) /* If return from fork() is negative – error */


{ /* Error occurred - no child process. */
fprintf( stderr, "Fork failed." ); /* Error message to stderr */
return 1;
}

if ( justPid == 0 )
{ /* Child process */
myPid = getpid(); /* To get PID of this process */
myParentPid = getppid(); /* To get the PID of parent process – PPID */ /* Rest of the code which runs as child
process. */
...
return( 0 );
}
else
{ /* Parent process - the value returned by fork() is child's PID */
myPid = getpid();
myParentPid = getppid();
/* Rest of the code which runs as parent process. */
...
return( 0 );
}
}
O processo filho recém-criado como uma imagem binária na memória, ou seja, é criado como um
clone de seu pai. Isso significa que ele herda as cópias de todas as variáveis criadas no pai (justPid,
myPid, myParentPid, no exemplo dado acima) com seus valores no momento da invocação da
função fork (). O mesmo é verdade no que diz respeito às funções implementadas no programa pai
(função main () neste caso).

Para compilar um programa simples em linguagem C (como o mostrado acima), os programadores


podem invocar o utilitário gcc a partir da linha de comando:

>gcc source_file.c –o executable_file

Para trocar o código binário do processo filho por algo diferente do código do programa
principal (pai), o programador pode usar a função da família exec (), que carrega o arquivo binário
dado como a imagem do filho:

justPid = fork();
...
if ( justPid == 0 )
{ /* Child process */
/* Loading new code (program) for child process and starting it */
execlp( "./ex_2-2b", "ex_2-2b", NULL );
}
else
{ /* Parent process */
...

Diferentes estratégias podem ser aplicadas quando vários processos relacionados estão
trabalhando juntos. Por exemplo: eles podem ser executados de forma independente e cada um
deles faz seu próprio trabalho, ou o pai pode esperar que seus processos filhos não façam nada e, em
seguida, fazer uso dos resultados fornecidos pelos processos filhos. Essa segunda estratégia é
implementada com a função wait () usada no processo pai. O código apropriado na variante mais
simples pode ter a seguinte aparência:

justPid = fork();
...
if ( justPid == 0 )
{ /* Child process */
printf( "[CHILD]: Child %d started.\n",
getpid() );
...
return( 0 );
}
else
{ /* Parent process waits for the end of child process: */
wait ( NULL );
printf( "[PARENT]: Child %d finished.\n", justPid );
}
...
Algumas observações interessantes podem ser realizadas com o seguinte exemplo de
programa:

#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#include <stdio.h>
#include <unistd.h>

int main()
{
pid_t justPid, myPid, myParentPid;
int exitStatus;

/* fork new process */


justPid = fork();

if ( justPid < 0 )
{ /* Error occurred - no child process. */
fprintf( stderr, "Fork failed." );
return 1;
}

if ( justPid == 0 )
{ /* Child process. */ myPid = getpid();

myParentPid = getppid();
printf( "[CHILD]: PID = %d PPID = %d\n", myPid, myParentPid );
/* Blind loop - signal like SIGINT is needed to exit from process. */
while ( 1 )
{
/* Empty loop - child process "works" */ ;
}
}
else
{ /* Parent process - the value returned by fork() is child's PID. */
myPid = getpid();
myParentPid = getppid();
printf( "[PARENT]: PID = %d PPID = %d Child's PID = %d\n", \ myPid, myParentPid,
justPid );
/* Wait for particular child process end. */
waitpid( justPid, &exitStatus, 0 );
if( WIFEXITED(exitStatus) )
printf("[PARENT]:Child process PID = %d exited with status %d\n", justPid, WEXITSTATUS(exitStatus));
if( WIFSIGNALED(exitStatus) )
printf("[PARENT]:Child process PID=%d terminated by signal %d\n",justPid,
WTERMSIG(exitStatus));
/* Then we can do something more (reminder section). */ while ( 1 )
{
/* Empty loop - parent process "works" */
;
}
return( 0 );
}
}
2. Criando um processo leve (thread) com as funções da API do UNIX.
Para criar um encadeamento de acordo com o programador padrão POSIX, é possível usar a
função da linguagem C pthread_create () disponível na biblioteca pthread da Interface do
Programador de Aplicativos UNIX. Esta preparação de tempo e inicialização apropriada do
ambiente necessário para manter o segmento ativo sob seu controle de processo pai é necessário.
Portanto, as funções pthread_attr_init () e pthread_attr_setscope () devem ser chamadas antes. A
implementação típica da criação de dois novos encadeamentos é assim:

#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>

void *threadCode1()
{ /* The code which should be run as thread #1 */
...
}

void *threadCode2()
{ /* The code which should be run as thread #2 */
...
}

int main()
{
pthread_t threadId1, threadId2;
pthread_attr_t threadAttr1, threadAttr2;

/* Preparation the environment for both threads: */


pthread_attr_init( &threadAttr1 );
pthread_attr_setscope( &threadAttr1, PTHREAD_SCOPE_PROCESS );
pthread_attr_init( &threadAttr2 );
pthread_attr_setscope( &threadAttr2, PTHREAD_SCOPE_PROCESS );

/* Creation of the threads: */


pthread_create(&threadId1,&threadAttr1,threadCode1,NULL);
pthread_create( &threadId2, &threadAttr2, threadCode2, NULL );

/* Rest of the main process program */


...
}
Para compilar o programa e usar a biblioteca pthread, deve-se usar o utilitário gcc da linha de
comando desta forma:

>gcc source_file.c –o executable_file –lpthread

Observe que não há espaço entre a opção -l e o nome da biblioteca vinculada.

A função pthread_attr_setscope() é importante, pois define a estratégia de compartilhamento de


tempo entre as threads e o processo principal. Ao usar PTHREAD_SCOPE_PROCESS, esse thread
rivaliza com o tempo de CPU do pool fornecido pelo sistema operacional para seu processo pai
principal. O valor PTHREAD_SCOPE_SYSTEM força a thread a rivalizar com todos os processos
em execução no sistema operacional (às vezes pode ser uma estratégia melhor). Sem definir este
atributo, o segmento não receberá o tempo de CPU, a menos que o processo pai o liberte sozinho
(aguardando a operação de E / S, por exemplo).
3. Comunicação entre processos simples com sinais.
Para enviar um sinal do programador da aplicação pode-se usar a função kill(), que funciona
como o comando kill shell - envia o sinal do dado número para o processo identificado pelo seu
PID. O programador também pode usar a função signal() para conectar as funções (manipuladores)
a determinados sinais que estarão funcionando em vez dos sinais padrão, exceto o sinal 9
(SIGKILL) que não pode ser reprogramado e sempre mata o processo imediatamente.

Implementação típica se parece com isso:


#include <sys/types.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

unsigned long i = 0; /* Public variable, accessible for all functions */

/* Custom SIGINT (Ctrl+C) signal handler */ void


myInt( int signo ){
if( signo == SIGINT ){
printf( "Process %d killed after %ld iterations.\n", getpid(), i ); exit( 0 );
}
}

int main(){
signal( SIGINT, myInt ); /* Setting a new handler for SIGINT */

printf( "Process %d started.\n", getpid() );

/* Blind loop - SIGINT signal aborts */


while ( 1 )
i++; /* No output by printf() etc. – CPU oriented process */
}

Tente executar o programa como descrito acima (modelo de um processo orientado por CPU)
em primeiro plano e mate-o usando Ctrl + C para enviar o sinal SIGINT, execute-o em segundo
plano e use o comando kill SIGINT <PID> depois de verificar o valor PID .

Na verdade, o processo de terminação precisa de algum suporte do pai do processo finalizado


para evitar indicá-lo como zumbi. Portanto, a implementação adequada do manipulador de sinal
deve ser mais ou menos assim:

#include <sys/types.h>
#include <sys/wait.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

/* Custom signal handler */


void mySignal( int signo )
{
if( signo == SIGTERM ) {
printf( "Process %d killed by SIGTERM.\n", getpid() ); exit( 0 ); /* exit() termitanes
the process */
}
if( signo == SIGINT ) {
printf( "Process %d killed by SIGINT.\n", getpid() ); exit( 0 );
}
/* SIGCHLD needs some support for terminated process */ if( signo == SIGCHLD ) {
int saved_errno;

saved_errno = errno;
while ( waitpid( (pid_t)-1, 0, WNOHANG ) > 0 ) {
}
errno = saved_errno;
printf( "Process %d received SIGCHLD.\n", getpid() );
}
}

int main()
{
pid_t justPid, myPid, myParentPid;

/* Setting custom handler for SIGTERM, SIGINT and SIGCHLD */ signal( SIGTERM,
mySignal );
signal( SIGINT, mySignal );
signal( SIGCHLD, mySignal );

justPid = fork(); /* New process is created at this moment */

if( justPid < 0 ) {


fprintf( stderr, "Fork failed.\n" );
return( 1 );
}

if ( justPid == 0 ) { /* Child process */ myPid = getpid();


myParentPid = getppid();
printf( "Child works with PID %d\n", myPid ); do {
} while( 1 ); return( 0 );
}
else { /* Parent process */
myPid = getpid();
myParentPid = getppid();
signal( SIGINT, mySignal );
printf( "Parent works with PID %d\n", myPid ); do {
} while( 1 ); return( 0 );
}
}
Exercício 1.

Leia o manual (man) sobre as funções de API UNIX/Linux fork(), getpid(), getppid(). Em seguida,
escreva um programa que crie um processo filho e, em seguida, pai e filho estejam executando em
loops infinitos exibindo seus PIDs e PPIDs. Dessa forma, você terá os modelos de processos típicos
orientados a E/S. Realize algumas observações, como o tempo de CPU é compartilhado entre esses
dois processos e como eles estão reagindo para Ctrl + C (SIGINT) ou sinal SIGTERM padrão
(enviado da linha de comando pelo comando kill shell quando executado em segundo plano). A tela
de saída pode ficar assim:

[PARENT]: PID 2374, PPID 2360


[PARENT]: PID 2374, PPID 2360
[CHILD]: PID 2375, PPID 2374
[PARENT]: PID 2374, PPID 2360
[CHILD]: PID 2375, PPID 2374
[CHILD]: PID 2375, PPID 2374
[PARENT]: PID 2374, PPID 2360
...

Exercício 2.

Leia o manual sobre as funções wait() e exec(). Então escreva um programa que crie um
processo filho, carregue um novo código binário (compilado de outra fonte) no filho, e então o pai
esperará (sem CPU ou E/S) até que o filho termine sua tarefa (contando de 1 a 100 e exibindo o
status do contador, por exemplo). A tela de saída pode ficar assim:
[PARENT]: PID 2421, waits for child with PID 2422
[CHILD]: PID 2421, starts counting:
[CHILD]: i = 1
[CHILD]: i = 2
...
[CHILD]: i = 100
[PARENT]: Child with PID 2422 finished and unloaded.

Exercício 3.
Leia o manual sobre as funções kill() e signal(). Então escreva um programa que cria um
processo filho, então pai e filho estão executando em loops incrementando suas próprias cópias do
contador declarado como variável pública (global) sem exibi-lo (modelos de um processo orientado
pela CPU) até o sinal SIGINT (Ctrl + C) mata os dois. O manipulador personalizado para o sinal
SIGINT deve exibir o valor do contador e sair do processo. A tela de saída pode ficar assim:

[PARENT]: PID 8519, starts counting.


[CHILD]: PID 8520, PPID 8519 starts counting.
Process 8519 killed, i =1874382172
Process 8520 killed, i = 1930216305

Realize vários experimentos com diferentes períodos de tempo de execução e tente tirar
algumas conclusões sobre a qualidade da multitarefa de compartilhamento de tempo em seu sistema
operacional: os processos são tratados igualmente pelo escalonador de processos?
Exercício 4.
Leia o manual sobre as funções pthread_attr_init(), pthread_attr_setscope() e
pthread_create(). Em seguida, escreva o programa que cria dois threads da seguinte forma:
primeiro deles incrementa a variável comum no loop infinito (variáveis globais desta vez não
são clonadas, mas acessíveis em uma instância para o processo principal e threads), o segundo
thread decrementa a mesmo variável. Ambos devem também contar o número de suas
iterações, o mesmo deve fazer o processo principal (três contadores diferentes). Todos eles
devem ser orientados a CPU (isso significa trabalhar sem qualquer saída para o terminal). O
final do processamento será causado pelo sinal SIGINT. O manipulador personalizado de
SIGINT deve exibir o valor final da variável comum (qual segmento é o vencedor?) E
números de iterações para as threads e processo principal. A tela de saída pode ficar assim:
[MAIN]: PID = 8902, i = 396891444
[MAIN]: main segment iterations: 506370425
[MAIN]: incrThread iterations: 732214172
[MAIN]: decrThread iterations: 409308229

Realize vários experimentos com diferentes períodos de tempo de execução e tente fazer
algumas conclusões se os threads e o processo principal são tratados de forma mais ou menos
igual.
Tente usar as configurações PTHREAD_SCOPE_PROCESS e
PTHREAD_SCOPE_SYSTEM.

Etapa 2: relatório

Escreva um relatório descrevendo, detalhadamente, o que foi feito nas duas etapas anteriores.
O intuito aqui é demonstrar, textualmente, a compreensão dos conceitos e capacidade de
aplicá-los.

A estrutura do relatório deve ser similar ao modelo a seguir:


 Resumo, Índice, Introdução (dizendo o que o trabalho faz);
 Conteúdo
◦ Apresentação dos códigos-fonte documentados;
◦ Apresentação das saídas apresentadas. Estes resultados devem ser tabulados,
acompanhados por prints das telas de saída e explicados.
 Conclusões (demonstrar o que foi aprendido por meio do trabalho);
 Referências (listar as referências realmente utilizadas para o desenvolvimento do
trabalho).

Você também pode gostar