Implementando Processos de Usuário em um Sistema Operacional

Fundamentos Teóricos

Em arquiteturas x86, cada tarefa pode ter um TSS (State Segment Task) e um LDT (Local Descriptor Table) associados. Os descritores de segmento para TSS e LDT residem na GDT (Global Descriptor Table). Uma troca de tarefa pode ser acionada por interrupções que invocam uma porta de tarefa, contendo o seletor do descritor TSS alvo.

Os registradores TR e LDTR contêm os seletores, dentro da GDT, dos descritores da TSS e LDT da tarefa corrente, respectivamente. O LDT armazena segmentos de código e dados exclusivos daquela tarefa.

A abordagem nativa de troca de tarefa é ineficiente. Ela requer salvar o estado completo da CPU no TSS antigo, carregar o estado do novo TSS e atualizar os registradores TR e LDTR. No entanto, a transição entre níveis de privilégio (ex: anel 3 para anel 0) depende do hardware obter o ponteiro da pilha de nível 0 do TSS atual.

O Linux adota uma estratégia otimizada: um único TSS por CPU. Durante uma troca de tarefa, apenas os campos SS0, esp0 e a máscara de E/S do TSS são atualizados. Como as trocas ocorrem sempre no nível 0, o contexto da tarefa antiga é salvo diretamente na pilha de nível 0. A funcionalidade do LDT é substituída por segmentos na GDT.

Uma tarefa pode possuir partes de diferentes privilégios (código de usuário e código do kernel). A mudança de privilégio dentro de uma mesma tarefa não constitui uma troca de tarefa, mas utiliza pilhas de nível diferentes, obtidas do TSS corrente.

Implementação Prática

A implementação do sistema operacional para este experimento gerencia um thread principal, dois threads do kernel e dois processos de usuário, totalizando cinco PCBs (Process Control Blocks). Threads do kernel possuem o campo pgdir nulo, enquanto processos de usuário apontam para suas próprias tabelas de páginas.

Escalonamento de Threads

A troca entre threads do kernel segue o mesmo mecanismo de escalonamento de multithreading. Antes da troca, a estrutura de pilha do thread no PCB é preparada, e PCB->kstack aponta para o topo desta pilha. Quando o thread atual esgota seu tempo, a rotina de escalonamento (schedule) executa. A metade superior da rotina switch_to salva o registrador esp atual no PCB->kstack do thread corrente. A metade inferior carrega esp a partir do topo da pilha pré-configurada do próximo thread, realizando pop dos valores para restaurar o contexto e executar a função do thread (kernel_thread), que por sua vez chama a função específica do thread (ex: k_thread_b). Durante todo esse processo, a execução permanece no nível de privilégio 0, utilizando a pilha de nível 0 do próprio thread. Note que, durante a execução de um thread do kernel, o esp aponta para dentro da sua pilha de thread, tornando o espaço reservado para a pilha de interrupção (acima) inutilizado.

Escalonamento de Processos

A criação de um processo de usuário, além de preparar a pilha do thread, também aloca memória para o bitmap de endereços virtuais e a tabela de páginas do processo, salvando os ponteiros no PCB.

O fluxo de escalonamento é aálogo ao dos threads até a execução de start_process. Esta função, ainda em nível 0, configura a pilha de interrupção do processo. Os campos CS, DS, SS são apontados para descritores de segmento de nível 3 (DPL=3). O ESP é definido como o topo da pilha de nível 3 do processo (obtida via alocação de página). O EIP aponta para a função do usuário (ex: u_prog_b). Finalmente, o esp é ajustado para o topo da pilha de interrupção configurada e um salto para intr_exit é dado. A rotina de saída de interrupção (intr_exit) pop os valores da pilha e executa iretd, efetivamente mudando para o nível de privilégio 3 e iniciando a execução da função do usuário com sua própria tabela de páginas.

Um ponto crucial: no modelo adotado, o TSS serve exclusivamente para armazenar o ponteiro da pilha de nível 0 do processo de usuário atual. Quando a rotina schedule determina o próximo processo (next), ela chama process_activate(next) antes de switch_to. Esta função atualiza o campo tss.esp0 com o endereço da pilha de nível 0 do processo (PCB + PG_SIZE) e troca o conteúdo do registrador CR3 para a tabela de páginas do processo. Quando o processo é interrompido (nível 3 -> nível 0), a CPU automaticamente obtém o novo esp de tss.esp0.

Diferenças entre Processo e Thread

As principais diferenças na implementação são:

  1. Após a troca via switch_to, um processo executa start_process que utiliza intr_exit/iretd para transferir a execução para o nível 3. Um thread do kernel continua no nível 0.
  2. Ao escalonar um processo de usuário, a rotina process_activate atualiza tss.esp0 e CR3. Para threads do kernel, CR3 é configurado para o endereço físico 0x100000 (espaço do kernel), e tss.esp0 não é alterado.
  3. A localização da pilha de interrupção difere. Para threads do kernel, a pilha de interrupção fica abaixo da pilha do thread (na página do PCB). Para processos, a pilha de interrupção fica acima, com o esp de nível 0 apontando para o topo da página do PCB.

Eexmplos de Código

Função Principal (main.c)

#include "interrupt.h"
#include "init.h"
#include "thread.h"
#include "print.h"
#include "process.h"
#include "console.h"

void kernel_thread_a(void* );
void kernel_thread_b(void* );
void user_process_a(void);
void user_process_b(void);

int counter_a = 0;
int counter_b = 0;

void main(void) {
    put_str("I am kernel\n");
    init_all();

    thread_start("kt_a", 31, kernel_thread_a, "argA ");
    thread_start("kt_b", 8, kernel_thread_b, "argB ");
    process_execute(user_process_a, "uproc_a");
    process_execute(user_process_b, "uproc_b");

    intr_enable();
    while(1);
}

void kernel_thread_a(void* arg){
    while(1){
        console_put_str("cnt_a:0x");
        console_put_int(counter_a);
    }
}

void kernel_thread_b(void* arg){
    while(1){
        console_put_str("cnt_b:0x");
        console_put_int(counter_b);
    }
}

void user_process_a(void) {
    while(1) {
        counter_a++;
    }
}

void user_process_b(void) {
    while(1) {
        counter_b++;
    }
}

Criação e Ativação de Processos (process.c)

#include "process.h"
#include "global.h"
#include "memory.h"
#include "thread.h"
#include "tss.h"
#include "string.h"

extern void intr_exit(void);

void init_process(void* entry_point) {
    void* func = entry_point;
    struct task_struct* cur = running_thread();
    cur->self_kstack += sizeof(struct thread_stack);
    struct intr_stack* proc_stack = (struct intr_stack*)cur->self_kstack;
    proc_stack->edi = proc_stack->esi = proc_stack->ebp = proc_stack->esp_dummy = 0;
    proc_stack->ebx = proc_stack->edx = proc_stack->ecx = proc_stack->eax = 0;
    proc_stack->gs = 0;
    proc_stack->ds = proc_stack->es = proc_stack->fs = SELECTOR_U_DATA;
    proc_stack->eip = func;
    proc_stack->cs = SELECTOR_U_CODE;
    proc_stack->eflags = (EFLAGS_IOPL_0 | EFLAGS_MBS | EFLAGS_IF_1);
    proc_stack->esp = (void*)((uint32_t)get_a_page(PF_USER, USER_STACK3_VADDR) + PG_SIZE);
    proc_stack->ss = SELECTOR_U_DATA;
    asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (proc_stack) : "memory");
}

void switch_page_directory(struct task_struct* task) {
    uint32_t dir_phy_addr = 0x100000; // Diretório de páginas do kernel (para threads)
    if (task->pgdir != NULL) {
        dir_phy_addr = addr_v2p((uint32_t)task->pgdir);
    }
    asm volatile ("movl %0, %%cr3" : : "r" (dir_phy_addr) : "memory");
}

void activate_process(struct task_struct* task) {
    ASSERT(task != NULL);
    switch_page_directory(task);
    if (task->pgdir) {
        update_tss_esp0(task);
    }
}

uint32_t* create_process_page_dir(void) {
    uint32_t* dir_vaddr = get_kernel_pages(1);
    if (dir_vaddr == NULL) { return NULL; }
    // Copiar entradas do kernel (3GB-4GB) do diretório atual
    memcpy((uint32_t*)((uint32_t)dir_vaddr + 0x300*4), (uint32_t*)(0xfffff000+0x300*4), 1024);
    uint32_t new_dir_phy = addr_v2p((uint32_t)dir_vaddr);
    dir_vaddr[1023] = new_dir_phy | PG_US_U | PG_RW_W | PG_P_1;
    return dir_vaddr;
}

void create_process_vaddr_bitmap(struct task_struct* proc) {
    proc->userprog_vaddr.vaddr_start = USER_VADDR_START;
    uint32_t bitmap_pages = DIV_ROUND_UP((0xc0000000 - USER_VADDR_START) / PG_SIZE / 8 , PG_SIZE);
    proc->userprog_vaddr.vaddr_bitmap.bits = get_kernel_pages(bitmap_pages);
    proc->userprog_vaddr.vaddr_bitmap.btmp_bytes_len = (0xc0000000 - USER_VADDR_START) / PG_SIZE / 8;
    bitmap_init(&proc->userprog_vaddr.vaddr_bitmap);
}

void process_execute(void* entry, char* name) {
    struct task_struct* pcb = get_kernel_pages(1);
    init_thread(pcb, name, default_prio);
    create_process_vaddr_bitmap(pcb);
    thread_create(pcb, init_process, entry);
    pcb->pgdir = create_process_page_dir();

    enum intr_status old_status = intr_disable();
    list_append(&thread_ready_list, &pcb->general_tag);
    list_append(&thread_all_list, &pcb->all_list_tag);
    intr_set_status(old_status);
}

Inicialização do TSS (tss.c)

#include "tss.h"
#include "global.h"
#include "string.h"

struct tss {
    uint32_t backlink;
    uint32_t* esp0;
    uint32_t ss0;
    uint32_t* esp1;
    uint32_t ss1;
    uint32_t* esp2;
    uint32_t ss2;
    uint32_t cr3;
    uint32_t (*eip) (void);
    uint32_t eflags;
    uint32_t eax;
    uint32_t ecx;
    uint32_t edx;
    uint32_t ebx;
    uint32_t esp;
    uint32_t ebp;
    uint32_t esi;
    uint32_t edi;
    uint32_t es;
    uint32_t cs;
    uint32_t ss;
    uint32_t ds;
    uint32_t fs;
    uint32_t gs;
    uint32_t ldt;
    uint32_t trace;
    uint32_t io_base;
};
static struct tss tss;

void update_tss_esp0(struct task_struct* task) {
    tss.esp0 = (uint32_t*)((uint32_t)task + PG_SIZE);
}

static struct gdt_desc build_descriptor(uint32_t* base, uint32_t limit, uint8_t low_attr, uint8_t high_attr) {
    uint32_t base_val = (uint32_t)base;
    struct gdt_desc desc;
    desc.limit_low_word = limit & 0x0000ffff;
    desc.base_low_word = base_val & 0x0000ffff;
    desc.base_mid_byte = ((base_val & 0x00ff0000) >> 16);
    desc.attr_low_byte = low_attr;
    desc.limit_high_attr_high = (((limit & 0x000f0000) >> 16) + high_attr);
    desc.base_high_byte = base_val >> 24;
    return desc;
}

void tss_init(void) {
    uint32_t tss_size = sizeof(tss);
    memset(&tss, 0, tss_size);
    tss.ss0 = SELECTOR_K_STACK;
    tss.io_base = tss_size;

    // Adicionar descritores na GDT (posições 4, 5, 6)
    *((struct gdt_desc*)0xc0000920) = build_descriptor((uint32_t*)&tss, tss_size - 1, TSS_ATTR_LOW, TSS_ATTR_HIGH);
    *((struct gdt_desc*)0xc0000928) = build_descriptor((uint32_t*)0, 0xfffff, GDT_CODE_ATTR_LOW_DPL3, GDT_ATTR_HIGH);
    *((struct gdt_desc*)0xc0000930) = build_descriptor((uint32_t*)0, 0xfffff, GDT_DATA_ATTR_LOW_DPL3, GDT_ATTR_HIGH);

    uint64_t gdt_operand = ((8 * 7 - 1) | ((uint64_t)(uint32_t)0xc0000900 << 16));
    asm volatile ("lgdt %0" : : "m" (gdt_operand));
    asm volatile ("ltr %w0" : : "r" (SELECTOR_TSS));
}

Modificação no Escalonador (thread.c)

void schedule() {
    next->status = TASK_RUNNING;
    activate_process(next); // Ativa CR3 e TSS.esp0 se necessário
    switch_to(next);
}

Tags: operating-system x86 process user-mode task-switching

Publicado em 8-4 15:35