Ming's Blog

Back

操作系统真象还原笔记——进程管理Blur image

线程相关类

pid池

用于管理pid的分配。

/* pid的位图,最大支持1024个pid */
uint8_t pid_bitmap_bits[128] = {0};

/* pid池 */
struct pid_pool {
   struct bitmap pid_bitmap;  // pid位图
   uint32_t pid_start;	      // 起始pid
   struct lock pid_lock;      // 分配pid锁
}pid_pool;
c

中断栈

此结构用于中断发生时保护程序(线程或进程)的上下文环境,进程或线程被外部中断或软中断打断时,会按照此结构压入上下文寄存器,intr_exit 中的出栈操作是此结构的逆操作,此栈在线程自己的内核栈中位置固定,所在页的最顶端。

线程栈

线程自己的栈,用于存储线程中待执行的函数。此结构在线程自己的内核栈中位置不固定,用在 switch_to 时保存线程环境。实际位置取决于实际运行情况。

PCB

双向链表

数据结构

/**********   定义链表结点成员结构   ***********
*结点中不需要数据成元,只要求前驱和后继结点指针*/
struct list_elem {
   struct list_elem* prev; // 前躯结点
   struct list_elem* next; // 后继结点
};

/* 链表结构,用来实现队列 */
struct list {
/* head是队首,是固定不变的,不是第1个元素,第1个元素为head.next */
   struct list_elem head;
/* tail是队尾,同样是固定不变的 */
   struct list_elem tail;
};
c

双向链表操作

线程管理

pid管理

pid池的管理实际上用位图实现,细节可见: http://121.40.138.116/wp-blog/computer-architecture/minios-mm-255.html#toc-head-7

线程初始化

  1. 首先对线程结构对应的内存清零,再 allocate_pid 分配 pid
  2. 判断传入线程是否为主线程,若是则设置为 TASK_RUNNING ,否则为 TASK_READY
  3. 初始化线程各类参数:
    • self_kstack 被设置为 (uint32_t)pthread + PG_SIZE
    • priorityticks 被设为相同的 prio 值,elapsed_ticks 设0.其中 ticks 表示线程在处理器上执行的时钟滴答数,elapsed_ticks 为线程已在CPU上执行的时间。
    • pgdir 进程自己页表的虚拟地址设为 NULL
    • parent_pid 设为-1表示该函数初始化的进程没有父进程。

创建线程

/* 初始化线程栈thread_stack,将待执行的函数和参数放到thread_stack中相应的位置 */
void thread_create(struct task_struct* pthread, thread_func function, void* func_arg) {
   /* 先预留中断使用栈的空间,可见thread.h中定义的结构 */
   pthread->self_kstack -= sizeof(struct intr_stack);

   /* 再留出线程栈空间,可见thread.h中定义 */
   pthread->self_kstack -= sizeof(struct thread_stack);
   struct thread_stack* kthread_stack = (struct thread_stack*)pthread->self_kstack;
   kthread_stack->eip = kernel_thread;
   kthread_stack->function = function;
   kthread_stack->func_arg = func_arg;
   kthread_stack->ebp = kthread_stack->ebx = kthread_stack->esi = kthread_stack->edi = 0;
}
c
  1. 首先预留出中断栈和线程栈的空间,即用 self_kstack 值减去对应栈结构的空间大小。
  2. self_kstack 指向地址作为线程栈首地址进行初始化,对 eip,执行函数,函数参数,ebp,ebx,esi,edi进行设置。

线程开始

  1. get_kernel_pages 分配给线程1页内存。
  2. init_thread 初始化线程基本信息。
  3. thread_create 初始化线程栈与执行函数相关信息。
  4. 加入就绪队列与全部线程队列并预先检查是否重复。
  5. 返回线程指针。
/* 由kernel_thread去执行function(func_arg) */
static void kernel_thread(thread_func* function, void* func_arg) {
/* 执行function前要开中断,避免后面的时钟中断被屏蔽,而无法调度其它线程 */
   intr_enable();
   function(func_arg);
}
c

用于执行 function 函数。

任务调度

线程管理一节实现了对单个线程的创建,在这节开始多线程的实现。 miniOS采用的是RR时间片轮转调度,每个线程执行预定的 ticks 时间就会被换下。

线程标识

对于线程在链表中的标识,我们不直接用其结构体作结点(这样太大),而是用

/* general_tag的作用是用于线程在一般的队列中的结点 */
   struct list_elem general_tag;
/* all_list_tag的作用是用于线程队列thread_all_list中的结点 */
   struct list_elem all_list_tag;
c

作为结点,这样每个结点只有两个指针大小,是可以接受的。

同时,可以用 elem2entry 宏获得对应PCB指针:

#define offset(struct_type,member) (int)(&((struct_type*)0)->member)
#define elem2entry(struct_type, struct_member_name, elem_ptr) \
	 (struct_type*)((int)elem_ptr - offset(struct_type, struct_member_name))
c

offset 获取结构体成员在结构体内的偏移,elem2entry 通过结构体成员指针减去其偏移值获得结构体在内存中的首地址(原理其实就是linux里的 container_of 宏,以下列出)。

中断处理函数

对于任务的时间计算,用硬件的时钟中断事件来处理。

/* 时钟的中断处理函数 */
static void intr_timer_handler(void) {
   struct task_struct* cur_thread = running_thread();

   ASSERT(cur_thread->stack_magic == 0x19870916);         // 检查栈是否溢出

   cur_thread->elapsed_ticks++;	  // 记录此线程占用的cpu时间嘀
   ticks++;	  //从内核第一次处理时间中断后开始至今的滴哒数,内核态和用户态总共的嘀哒数

   if (cur_thread->ticks == 0) {	  // 若进程时间片用完就开始调度新的进程上cpu
      schedule();
   } else {				  // 将当前进程的时间片-1
      cur_thread->ticks--;
   }
}
c
  1. 获取当前线程pcb指针。
  2. 检测结构体末尾魔数,若改变则说明栈溢出。
  3. 增加 elapsed_ticks 同时减少 ticks ,预先检查 ticks 归零时,使用 schedule 切换进程。

schedule

  1. 获取线程指针并加入 thread_ready_list,重置 ticksstatus
  2. thread_ready_list 为空,则唤醒空闲线程 idle
  3. 弹出 thread_ready_list 队首线程并获取其指针。
  4. 激活任务列表并且切换。

process_activate

/* 击活线程或进程的页表,更新tss中的esp0为进程的特权级0的栈 */
void process_activate(struct task_struct* p_thread) {
   ASSERT(p_thread != NULL);
   /* 击活该进程或线程的页表 */
   page_dir_activate(p_thread);

   /* 内核线程特权级本身就是0,处理器进入中断时并不会从tss中获取0特权级栈地址,故不需要更新esp0 */
   if (p_thread->pgdir) {
      /* 更新该进程的esp0,用于此进程被中断时保留上下文 */
      update_tss_esp(p_thread);
   }
}
c
  1. page_dir_activate 激活其页表。
  2. p_thread->pgdir 不为 NULL,则为进程,更新该进程的 esp0

page_dir_activate

update_tss_esp

/* 更新tss中esp0字段的值为pthread的0级线 */
void update_tss_esp(struct task_struct* pthread) {
   tss.esp0 = (uint32_t*)((uint32_t)pthread + PG_SIZE);
}
c

switch_to

switch_to 主要任务是备份当前进程上下文环境,并且恢复接下来进程的上下文环境。

用户进程

内存管理

见:http://121.40.138.116/wp-blog/computer-architecture/minios-mm-255.html#toc-head-8

初始化

  1. self_kstack += sizeof(struct thread_stack) 使 self_kstack 指向 intr_stack 最低处。
  2. 参数初始化:
    • cs 设为预设的 SELECTOR_U_CODE 即段描述符,((5 << 3) + (TI_GDT << 2) + RPL3)
    • esp 起始地址设为预设的 (0xc0000000 - 0x1000)
    • ss 设为预设的 SELECTOR_U_DATA((6 << 3) + (TI_GDT << 2) + RPL3)

用户进程管理

  1. 在内核空间分配一页存储页表。
  2. 复制内核页表内容。
  3. 把最后一个页表项的内容改为本页表的起始物理地址。
  1. 初始化进程
  2. 创建用户进程虚拟地址位图
  3. 创建进程并创建页表
  4. 初始化堆内存
  5. 关中断并加入任务列表。

匿名管道

匿名管道只对创建它的进程以及其子进程可见,因而局限于父子进程的通信。为方便这里只实现匿名管道。

环形缓冲区

/* 环形队列 */
struct ioqueue {
// 生产者消费者问题
    struct lock lock;
 /* 生产者,缓冲区不满时就继续往里面放数据,
  * 否则就睡眠,此项记录哪个生产者在此缓冲区上睡眠。*/
    struct task_struct* producer;

 /* 消费者,缓冲区不空时就继续从往里面拿数据,
  * 否则就睡眠,此项记录哪个消费者在此缓冲区上睡眠。*/
    struct task_struct* consumer;
    char buf[bufsize];			    // 缓冲区大小
    int32_t head;			    // 队首,数据往队首处写入
    int32_t tail;			    // 队尾,数据从队尾处读出
};
c

管道实现

把管道视为一种特殊文件,把管道文件fd_flags 的值设为 0xFFFF,不再是 ORDONLY、O_WRONLY 等值。再把文件结构中的 fd_inode 指向管道的内存缓冲区, fd pos 则用于此管道的打开数。

管道读写

fork系统调用

fork()实际上调用sys_fork系统调用。

首先为子进程分配1页内存空间存储 PCB,再调用 copy_process()

copy_process

  1. 先用 copy_pcb_vaddrbitmap_stack0() 复制父进程的pcb、虚拟地址位图、内核栈到子进程。
  2. 再用 create_page_dir() 为子进程创建页表。
  3. copy_body_stack3() 复制父进程进程体及用户栈给子进程。
  4. build_child_stack() 创建子进程。
  5. update_inode_open_cnts() 更新文件inode的打开数。
  6. mfree_page() 清理。

copy_pcb_vaddrbitmap_stack0

  1. 先复制 pcb 所在页。
  2. 再修改新进程各项参数。
    • fork_pid 调用 alloc_pid 从pid位图中分配子进程pid。
    • 设置进程优先级,时间片,状态。父进程pid等参数。
  3. block_desc_init 初始化堆内存。
  4. 分配内存地址给虚拟地址位图,复制父进程位图内容,并将子进程虚拟位图指针指向它。

create_page_dir

  1. 复制内核页目录项(768-1022)
  2. 把最后一项——第1023项设为新页目录的物理地址。
  3. 返回页目录虚拟地址。

copy_body_stack3

build_child_stack

update_inode_open_cnts

  1. 遍历除了三个标准文件描述符之外所有文件描述符,获得其在全局描述符表的下标。
  2. 根据下标在 file_table 中找到对应文件,使对应文件结构的 fd_nodei_open_cnts 加一。

mfree_page

操作系统真象还原笔记——进程管理
https://astro-pure.js.org/blog/eleos-process
Author Ming
Published at February 23, 2024
Comment seems to stuck. Try to refresh?✨