Programação Multi-processo em Unix

André Moreira ([email protected])
Professor Adjunto do Departamento de Engenharia Informática do ISEP

Processos em Unix

Um processo é um componente de "software" em execução sob o controlo do núcleo do sistema operativo. Cada processo tem associado a si um contexto, constiuido por zona de dados, código e pilha.

O contexto de um processo é inacessivel a outros processos, do mesmo modo um processo apenas pode actuar sobre o seu contexto. Um processo comunica com o mundo exterior sob vigilancia do sistema operativo, assim assegura-se um funcionamento estável, sem interferências entre processos.

Do contexto de um processo faz parte diverso tipo de informação de controlo, de entre ela interessa neste momento destacar o Identificador do Processo (PID) e o Identificador de Utilizador (UID):

O PID ("Process IDentifier") é número inteiro positivo que identifica um dado processo, é atribuido pelo sistema operativo quando o processo é criado.

O UID ("User IDentifier") é um número inteiro positivo que identifica um utilizador, é atribuido pelo sistema operativo ou pelo administrador quando o utilizador é definido. Ao UID ficam associados uma serie de direitos de utilização. Um processo adquire o UID do utilizador que o cria, ficando por isso com os mesmos direitos.

Quando um utilizador cria um processo por invocação de um comando, com carregamento para a memória do respectivo ficheiro executável, o processo adquire o UID e direitos do invocador, contudo, quando o proprietário ("owner") de um ficheiro executável o desejar pode permitir que o processo correspondente assuma o seu UID, seja qual for o utilizador que o invoca (existem obvios riscos de segurança para o propriétário).

Inicio de novos processos

Existe uma única "system-call" que assegura esta função, trata-se de uma primitiva base para qualquer sistema operativo multi-processo:

		int fork(void);

Quando um processo invoca esta "system-call" o sistema operativo cria uma cópia do contexto do processo corrente e atribui-lhe um novo PID, apartir desse momento existem dois processo exactamente iguais. Ao processo original (mantém o mesmo PID) chamamos processo pai, ao novo processo (com um novo PID) chamamos processo filho.

Certamente que o código que se pretende executar depois do "fork" é diferente para o pai e para o filho, para que o código em execução saiba qual o processo em que está basta analizar o valor de retorno do "fork":

  • Ao processo pai a "system-call" "fork" devolve o PID do filho (inteiro positivo).
  • Ao processo filho a "system-call" "fork" devolve o valor zero.

É tipico inserir a "system-call" "fork" numa função "if":

	if(fork())
		{
		/* continuação do
		   processo pai   */
		}
	else
		{
		/* continuação do
		   processo filho */
		}

Qualquer processo pode obter facilmente o seu próprio PID e também o PID do seu pai. A "system-call" int getpid(void) devolve ao processo invocador o seu próprio PID. A "system-call" int getppid(void) devolve ao processo invocador o PID do seu pai.

O exemplo seguinte ilustra a utilização do "fork" e destas duas "system-calls":

void main(void)
{
int fPid;
printf("Ainda existe apenas um processo (%i)\n",getpid());
fPid = fork();
printf("Agora somos dois (%i)\n",getpid());
if(fPid)
	{
	printf("Eu sou o pai(%i), o meu filho: %i\n",getpid(),fPid);
	sleep(1);
	}
else
	{
	printf("Eu sou o filho(%i), o meu pai: %i\n",getpid(),getppid());
	}
}

A "system-call" "sleep" é usada para adormecer o processo pai durante um segundo, assegurando que o pai não termina antes do filho.

Sincronização entre processo pai e filhos

No exemplo anterior o processo pai foi suspenso durante um segundo para ter certas garantias que o filho acaba primeiro. A consequência de isto não acontecer é que se a "system-call" getppid() fosse invocada depois de o pai ter terminado, devolveria um valor inesperado. Na realidade devolveria o PID do "pai do pai" já que seria este a adoptar o "filho orfão".

Existem mecanismos muitos mais potentes e expeditos de assegurar o sincronismo entre pai e filhos. A "system-call" "wait" suspende o processo pai até que um filho termine:

	int wait(int *);

O valor de retorno desta "system-call" é o PID do filho que terminou. Se já não existe mais nenhum filho a "system-call" "wait" devolve o valor -1.

A "system-call" "wait" tem como parâmetro um apontador para um inteiro que será usado para guardar o "exit-status" do filho. Este mecanismo permite ao filho, no momento em que termina enviar ao pai um valor numérico, na prática está limitado aos 8 bits menos significativos que terão de ser obtidos utilizando a "macro":

	int WEXITSTATUS(int);

Os filhos transmitem ao pai o seu código de saída utilizando a "system-call" "exit" que termina o processo:

	void exit(int);

Enquanto a invocação da "system-call" "exit" não for correspondida pela "system-call" "wait" no processo pai o processo filho fica num estado conhecido por "Zombie" em que não ocupa recursos.

O exemplo seguinte ilustra a utilização deste tipo de mecanismo:

void main(void)
{
int i, fPid, ret;
for(i=1;i<11;i++)
	{
	fPid = fork();
	if(!fPid)
		{
		printf("Sou o filho número %i\n",i);
		exit(i+10);
		}
	printf("Pai: lancei o filho %i com o PID %i\n",i,fPid);
	}

while((fPid=wait(&ret))!= -1)
	{
	ret=WEXITSTATUS(ret);
	printf("O filho com PID %i devolveu-me %i\n",fPid,ret);
	}
}

A transferência de dados do processo pai para o filho no momento da sua criação é simples porque todos os dados definidos pelo pai são copiados para o filho. A transferência de dados do processo filho quando este termina, para o processo pai é limitada a 8 bits. Este facto torna-se irrelevante porque existem mecanismos apropriados para a comunicação entre processos (IPC).

Funções "exec"

As funções "exec" utilizam outra primitiva básica, embora sejam variadas na forma dos seus parâmetros todas realizam a mesma tarefa: carregam de um ficheiro binário que substitui o código e dados do contexto do processo corrente e inicia a sua execução.

A menos que ocorra um erro, a execução do código no processo invocador termina. Quando se pretende executar um comando paralelamente ao processo principal basta criar um filho que invoca uma função "exec".

Existem duas formas de se especificar o ficheiro executável e respectivos argumentos, em lista (sufixo "l") ou sob a forma de um vector (sufixo "v"):

int execl(char *path, char *argv0, char *argv1, char *argv2, ..., NULL);
int execv(char *path, char *argv[]);

Em qualquer dos casos o parâmetro "path" representa o caminho para o local onde se encontra o ficheiro binário. O primeiro argumento (argv0 ou argv[0]) é sempre o nome do ficheiro binário.

A forma de indicar os argumentos varia, mas em qualquer dos casos termina com o argumento NULL ((char *)0).

As variantes com sufixo "e" permitem definir variáveis de ambiente para a execução do comando, a parte inicial dos parâmetros é idêntica às anteriores:

	int execle( ..., char *envp[]);
	int execve( ..., char *envp[]);

As variantes com sufixo "p" utilizam a variável de ambiente PATH para procurar o ficheiro executável. Neste caso o primeiro parâmetro passa a ser o nome do ficheiro executável, se este nome de ficheiro não contém o caminho, então a função procura-o na sequência de caminhos especificada na variável PATH:

int execlp(char *filename, char *argv0, char *argv1, char *argv2, ..., NULL);
int execvp(char *filename, char *argv[]);

Exemplos:

1) execl("/bin","ls","-lag","~/",NULL);
2) execlp("/bin/ls","ls","-lag","~/",NULL);
3) execlp("ls","ls","-lag","~/",NULL);

Os exemplos 1 e 2 são totalmente equivalentes. O exemplo 3 terá o mesmo efeito se o caminho "/bin" estiver na PATH.

Exemplo com vector:

...
char *arg[4];

arg[0]="ls";
arg[1]="-lag";
arg[2]="~/";
arg[3]=NULL;
execv("/bin",arg);
printf("A função EXEC falhou\n");
...

A mensagem de erro é pertinente para qualquer função "exec", se uma função "exec" tem sucesso, a instrução seguinte nunca é executada.

O exemplo seguinte cria um processo filho para executar o mesmo comando em paralelo:

...
char *arg[4];

arg[0]="ls";
arg[1]="-lag";
arg[2]="~/";
arg[3]=NULL;
...
if(!fork())
	{
	execvp(arg[0],arg);
	printf("A função EXEC falhou\n");
	exit(1);
	}
...
/* continuação do processo pai */