ResumosMIEI

Uma coleção de resumos, para ajudar os que precisam daquele empurrão.


Project maintained by mendess Hosted on GitHub Pages — Theme by mattgraham

Processos

Se ficheiros são a primeira primitiva do sistema, processos são a segunda (que também são ficheiros, mas explico isso mais a frente)

Um processo é um programa animado, nada mais. Para o executar o sistema operativo carrega-o para RAM e passa o controlo para o processo. Mas estes processos têm de estar organizados de alguma forma.

PID

Cada processo tem um id único chamado pid, que o identifica, e todos os processos podem consultar o seu com a função getpid.

pid_t getpid(void);

Parenting 101

Outra parte importante da organização dos processos é a noção de pai e filho, quando um processo quer criar outro processo tem de fazer um filho usando a chamada fork e tal como o pid do próprio, um processo pode consultar o pid do seu pai com a função getppid.

pid_t getppid(void);

Importante notar que um processo não pode consultar a qualquer momento os pids dos seu filhos, mas há outras formas de conseguir isto.

Forking

Para criar um processo novo, em sistemas Unix like, faz se uso da system call fork. Quando um processo faz fork, o sistema operativo cria uma cópia de toda a memoria [1] do forking process para um novo, e de seguida retoma a execução dos dois.

Exemplo

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

int main(void) {
    puts("Olá, estou sozinho!");
    fork();
    puts("Olá, não estou sozinho.");
}

O output deste programa é o seguinte:

Olá, estou sozinho!
Olá, não estou sozinho.
Olá, não estou sozinho.

Utilidade do fork

pid_t fork(void);

Outra implicação do fork, é que este “retorna duas vezes”, uma no processo pai e outra no processo filho, e o valor de retorno é diferente para cada um destes.

Para o pai, o fork retorna o pid do processo filho criado, para o filho retorna 0 para que este saiba que é o filho. Com isto podemos distinguir o filho do pai.

Exemplo

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

int main(void) {
    puts("Ola, estou sozinho!");
    pid_t const filho = fork();
    if (filho != 0) {
        printf(
            "Sou o pai, tenho pid %d, e o meu filho tem pid: %d\n",
            getpid(),
            filho
        );
    } else {
        printf(
            "Sou o filho, tenho pid %d, e o meu pai tem pid: %d\n",
            getpid(),
            getppid()
        );
    }
    puts("Ola, não estou sozinho.");
}

O output deste programa não é definido, porque a ordem pela qual os prints acontecem não é determinística. (Escrevam e compilem o programa para verificar).

Algo que é determinístico, no entanto, é que ambos vão escrever Ola, não estou sozinho, isto porque, enquanto que os ramos do if são exclusivos, todo o resto não é. É importante ter isto em conta para que não escrever código que faz coisas a mais. Principalmente, é importante para não criar fork bombs acidentalmente.

#include <unistd.h>

int main(void) {
    while (1) fork();
}

Este programa, quando executado, cria um filho, que por si vai criar mais filhos que vão criar mais filhos, etc… Até que o o número de pids esgota e a maquina fica inutilizável porque precisas de processos para matar processos.

Wait, zombies e órfãos

Criar processos filhos vem com responsabilidades da parte do processo pai.

Para esperar que um processo filho termine temos as funções:

pid_t wait(int* status);
pid_t waitpid(pid_t pid, int* status, int options);

Ambas retornam o pid do processo que terminou, e colocam no status o exit status desse processo. Em caso de erro, -1 é retornado.

Enquanto que o wait retorna mal encontre um processo filho que tenha terminado, o waitpid, por outro lado, permite ter mais controlo sobre quando retornar.

Parâmetro: pid

O parâmetro pid pode tomar valores que não pids, mas para além de -1 (que significa “qualquer processo”), estes saem fora da matéria da cadeira [2].

Parâmetro: options

Ao parâmetro options pode ser passado 0 para não ativar nenhuma opção, ou as opções especificadas na man page, das quais a mais útil é WNOHANG, que permite ao waitpid não bloquear caso nenhum processo filho tenha terminado.

Parâmetro: status

O parâmetro status é usado para capturar o exit status do processo que foi waited on.

Necessidade do wait

O que acontece ao processo filho se não fizermos wait?

Enquanto o processo pai existir os processos filhos que já terminaram são designados de zombies, o sistema operativo mantém apenas a informação necessária para que o processo possa ser waited for mais tarde. Mesmo assim, enquanto o zombie existir, uma entrada na tabela de processos continua a existir, e se esta tabela encher não é possível criar novos processos.

No caso de o pai terminar normalmente, sem ter feito wait de todos os seus filhos, estes tornam se órfãos e passam a ser filhos do processo init.

O init é o primeiro processo que arranca sempre que o sistema operativo inicia, tem sempre pid 1 e é o “pai de todos”. Ele encarrega-se de periodicamente chamar o wait para todos os seus filhos de forma a evitar ter zombies, desta forma também serve como um fall back plan para processos órfãos não se tornarem zombies mais tarde.

Exit status

Quando um programa termina retorna uma valor entre 0 e 255, 0 normalmente significa que não ocorreu nenhum erro e qualquer outro significa que algum erro ocorreu.

Este comportamento pode ser observado na shell, visto que esta é o pai dos comandos que nela são executados.

$ gcc main.c
$           # a variável especial $? serve para verificar
$ echo "$?" # o exit status do último programa
0           # correu tudo bem
$ cd ficheiro_que_nao_existe.c
gcc: error: ficheiro_que_nao_existe.c: No such file or directory
gcc: fatal error: no input files
compilation terminated.
$ echo "$?"
1           # ocorreu um erro

Para verificar o status de um processo, em C, podemos consultar a o valor do status que foi passado ao à função wait. Isto tem de ser feito com as macros WIFEXITED e WEXITSTATUS. Estas têm de ser usadas em conjunto para evitar undefined behaviour, sendo que WEXITSTATUS só pode ser usada caso WIFEXITED retornar 1 (true).

int status;
pid_t const pid = wait(&status);
if(pid != -1 && WIFEXITED(status)) {
    puts("Child exited normally");
    int const exit_status = WEXITSTATUS(status); // Esta macro so pode ser usada nas
                                                 // condições que o `if` acima garante
    printf("Exit status: %d\n", exit_status);
}

A razão para a ter de ser verificar se o processo terminou normalmente, é que este pode ter terminado por receber um sinal, por exemplo, SIGSEGV (segmentation fault), e neste caso o valor que vem em status não é um exit status valido.

Nota: É importantíssimo que nunca se observe o valor do status diretamente, isto deve ser feito sempre com recurso às macros definidas no wait.h.

Emitir um exit status

Do lado do processo filho, as formas de comunicar este valor para o seu pai pode ser feita das seguintes formas.

return na main

O valor retornado pela main é o exit status do processo.

int main(void) {
    return 1;
}

Este número, apesar de “ser um int”, tem de ser um número não negativo entre 0 e 255 (inclusive). Qualquer número que não esteja dentro deste intervalo vai ser reinterpretado dessa forma. Por exemplo, -1 pode ser interpretado como 255 se forem apenas considerados os low 8 bits do número.

exit e/ou _exit

As outras duas formas de terminar um processo com um exit status são com as funções exit. A exit é mais complexa, como explicado em man 3 exit:

Finalmente exit chama _exit. Por isso, _exit é mais abrupta.

Há ainda mais formas de terminar um processo mas deixo como exercício ao leitor a aprendizagem das mesmas:

Exemplos

Procurar em paralelo.

// Linux specific
#include <sys/types.h>
#include <sys/wait.h>

// POSIX specific
#include <unistd.h>

// C stdlib
#include <assert.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>

#define SIZE 1000 * 1024 * 1024UL
#define LINES 2

static char BIG_DATA[LINES][SIZE] = {0};

bool has_x(size_t line) {
    for(size_t j = 0; j < SIZE; j++) {
        if (BIG_DATA[line][j] == 'x') {
            printf("Found it at: (%zu,%zu)\n", line, j);
            return true;
        }
    }
    return false;
}

int main(int argc, char const* argv[]) {
    assert(argc > 2); // Can't bother handling lack of args
    // Putting the 'x' in the array to find it.
    // Usei `atoi` aqui para simplicidade, esta funcão não deve ser usada
    // porque não distingue erros do número 0. Ex: "ola" e "0" ambas são
    // interpretadas como 0. Alternativa: strtoi, strtol, strtof, etc
    BIG_DATA[atoi(argv[1]) % 2][atoi(argv[2])] = 'x';

    pid_t children[LINES];
    // Iniciar cada um dos filhos quer irão procurar o 'x'
    for (size_t i = 0; i < LINES; i++) {
        pid_t const pid = fork();
        if(pid == 0) {
            return has_x(i) ? EXIT_SUCCESS : EXIT_FAILURE;
        } else {
            // Guardar o pid de cada um para mais tarde...
            children[i] = pid;
        }
    }

    for(size_t i = 0; i < LINES; i++) {
        int status;
        // ... esperar por cada um para saber se encontraram ou não o 'x'
        pid_t const p = waitpid(children[i], &status, 0);
        // O waitpid tem de retornar o pid do processo que foi encontrado.
        // Logo se o `p` != `children[i]` então ocorreu algum erro ao esperar.
        // Apenas se pode consultar o exit status se o processo terminou normalmente
        // Logo WIFEXITED(status)
        if (p == children[i] && WIFEXITED(status)) {
            if(WEXITSTATUS(status) == EXIT_SUCCESS) {
                printf("Process #%zu found the 'x'\n", i);
            } else {
                printf("Process #%zu didn't find the 'x'\n", i);
            }
        } else {
            printf("Something went wrong with process #%zu\n", i);
        }
    }
}

Extra notes

  1. Apesar da semântica do fork ser que toda a memoria do processo é copiada, na verdade nenhuma é copiada, apenas quando um dos processos a tenta alterar é que esta é copiada e só depois é que é alterada. Esta técnica tem o nome de copy on write.

  2. Como explicado na man page do waitpid, este pode operar sobre process groups. Por defeito todos os processos pertencem ao mesmo grupo do processo pai, mas, por exemplo, a grande maioria das shells colocam os processos que o utilizador inicia em grupos diferentes da própria shell. Os grupos são importantes para a propagação de sinais, sendo possível enviar sinais a um grupo e não apenas a um processo.