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:
- Após a troca via
switch_to, um processo executastart_processque utilizaintr_exit/iretdpara transferir a execução para o nível 3. Um thread do kernel continua no nível 0. - Ao escalonar um processo de usuário, a rotina
process_activateatualizatss.esp0eCR3. Para threads do kernel,CR3é configurado para o endereço físico 0x100000 (espaço do kernel), etss.esp0não é alterado. - 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
espde 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);
}