[关闭]
@SovietPower 2021-12-05T14:52:27.000000Z 字数 7574 阅读 1233

OS pintos进程管理

OS



https://www.zybuluo.com/SovietPower/note/1912540


process.c

文件在userprog/process.c下。

段寄存器

32位CPU所含有的寄存器有
4个数据寄存器(EAX、EBX、ECX和EDX)
2个变址和指针寄存器(ESI和EDI) 2个指针寄存器(ESP和EBP)
6个段寄存器(ES、CS、SS、DS、FS和GS)
1个指令指针寄存器(EIP) 1个标志寄存器(EFlags)

计算机需要对内存分段,以分配给不同的程序使用。在描述内存分段时,需要有如下段的信息:段的大小、段的起始地址、段的管理属性(禁止写入/禁止执行/系统专用等)。
这些信息需要用8字节(64位)存储,但段寄存器只有16位,因此段寄存器中只存储段号(segment selector,段选择符),再由段号映射到存在内存中的GDT(全局描述符表)。

分类
代码段寄存器CS(Code Segment):存放当前正在运行的程序代码所在段的段基址,表示当前使用的指令代码可以从该段寄存器指定的存储器段中取得,相应的偏移量则由IP提供。
数据段寄存器DS(Data Segment):指出当前程序使用的数据所存放段的最低地址,即存放数据段的段基址。
堆栈段寄存器SS(Stack Segment):指出当前堆栈的底部地址,即存放堆栈段的段基址。
附加段寄存器ES(Extra Segment):指出当前程序使用附加数据段的段基址,该段是串操作指令中目的串所在的段。
标志段寄存器FS(Flag Segment):附加段寄存器。
全局段寄存器GS(Global Segment):附加段寄存器。

SEL_UCSEG, SEL_UDSEG, SEL_TSS, SEL_CNT

userprog/gdt.h中。GDT为全局描述符表。

  1. /* 段选择符. */
  2. #define SEL_UCSEG 0x1B /* 用户代码段选择符. */
  3. #define SEL_UDSEG 0x23 /* 用户数据段选择符. */
  4. #define SEL_TSS 0x28 /* 任务状态段,实现任务的挂起和恢复. */
  5. #define SEL_CNT 6 /* 段数. */

FLAG_MBS, FLAG_IF

标志寄存器值。

  1. /* EFLAGS Register. */
  2. #define FLAG_MBS 0x00000002 /* 必须设置. */
  3. #define FLAG_IF 0x00000200 /* 中断标志. */

intr_frame

中断帧,保存中断发生时进程的栈帧。

和任务状态段(Task State Segment, TSS)差不多?
任务状态段包含了多个字段,表示了管理任务的所有信息。和其它段一样,任务状态段描述符(TSS Descriptor)用于定义TSS。

  1. struct intr_frame
  2. {
  3. /* Pushed by intr_entry in intr-stubs.S.
  4. 保存中断程序的寄存器. */
  5. uint32_t edi; /* 保存 EDI. */
  6. uint32_t esi; /* 保存 ESI. */
  7. uint32_t ebp; /* 保存 EBP. */
  8. uint32_t esp_dummy; /* 未使用. */
  9. uint32_t ebx; /* 保存 EBX. */
  10. uint32_t edx; /* 保存 EDX. */
  11. uint32_t ecx; /* 保存 ECX. */
  12. uint32_t eax; /* 保存 EAX. */
  13. uint16_t gs, :16; /* 保存 全局段寄存器. */
  14. uint16_t fs, :16; /* 保存 标志段寄存器. */
  15. uint16_t es, :16; /* 保存 附加段寄存器. */
  16. uint16_t ds, :16; /* 保存 数据段寄存器. */
  17. /* Pushed by intrNN_stub in intr-stubs.S. */
  18. uint32_t vec_no; /* 中断向量标号. */
  19. /* Sometimes pushed by the CPU,
  20. otherwise for consistency pushed as 0 by intrNN_stub.
  21. The CPU puts it just under `eip', but we move it here. */
  22. uint32_t error_code; /* 错误码. */
  23. /* Pushed by intrNN_stub in intr-stubs.S.
  24. This frame pointer eases interpretation of backtraces. */
  25. void *frame_pointer; /* 保存 帧指针 EBP. */
  26. /* Pushed by the CPU.
  27. 保存中断程序的寄存器. */
  28. void (*eip) (void); /* 指令寄存器的位置. */
  29. uint16_t cs, :16; /* 保存 代码段寄存器. */
  30. uint32_t eflags; /* 保存 CPU 标志符. */
  31. void *esp; /* 保存栈指针. */
  32. uint16_t ss, :16; /* 保存 堆栈段寄存器. */
  33. };

process_execute()

开始一个线程,用于运行名为file_name的用户程序。
如果线程被成功创建,返回线程id,否则返回TID_ERROR
线程可能在该函数结束前就被调度运行。

  1. tid_t process_execute (const char *file_name)
  2. {
  3. char *fn_copy;
  4. tid_t tid;
  5. /* Make a copy of FILE_NAME.
  6. Otherwise there's a race between the caller and load(). */
  7. fn_copy = palloc_get_page (0);
  8. if (fn_copy == NULL)
  9. return TID_ERROR;
  10. strlcpy (fn_copy, file_name, PGSIZE); // 拷贝该参数,避免之后使用时被修改?
  11. /* Create a new thread to execute FILE_NAME. */
  12. tid = thread_create (file_name, PRI_DEFAULT, start_process, fn_copy); // 创建执行该程序的新线程
  13. if (tid == TID_ERROR)
  14. palloc_free_page (fn_copy);
  15. return tid;
  16. }

start_process()

加载一个用户程序并使其开始运行。
会初始化一个中断帧,调用load()加载程序,然后执行一个从中断的返回intr_exit()来开始该进程。

  1. static void start_process (void *file_name_)
  2. {
  3. char *file_name = file_name_;
  4. struct intr_frame if_;
  5. bool success;
  6. /* 初始化中断帧,并调用load()加载程序 */
  7. memset (&if_, 0, sizeof if_);
  8. if_.gs = if_.fs = if_.es = if_.ds = if_.ss = SEL_UDSEG;
  9. if_.cs = SEL_UCSEG;
  10. if_.eflags = FLAG_IF | FLAG_MBS;
  11. success = load (file_name, &if_.eip, &if_.esp);
  12. /* 加载失败,退出 */
  13. palloc_free_page (file_name);
  14. if (!success)
  15. thread_exit ();
  16. /* 通过模拟一个从中断的返回(intr_exit()),来开始这个用户进程。
  17. intr_exit() 需要中断帧的所有参数,所以只需将栈指针指向当前栈指,就可以直接跳到它。*/
  18. asm volatile ("movl %0, %%esp; jmp intr_exit" : : "g" (&if_) : "memory");
  19. /* 确保成功跳转,而不是继续向下执行 */
  20. NOT_REACHED (); // PANIC ("executed an unreachable statement");
  21. }

process_wait()

等待指定进程退出,并返回其退出状态。
若对应进程被内核终止,返回-1;如果指定的TID非法,或对应进程不是当前进程的子进程,或已经调用过了对应进程的process_wait(),不等待,立即返回-1

该函数还未实现。

  1. int process_wait (tid_t child_tid UNUSED);

pagedir_activate()

加载指定页目录PD的物理地址,到CPU的页目录基址寄存器中,使PD对应的新的页表立刻被激活。
pd为空指针,则令pd = init_page_dir,即加载预定义的页目录(为只有内核可以映射的页目录,kernel-only page directory)。

  1. void pagedir_activate (uint32_t *pd);

pagedir_destroy()

销毁指定页目录PD,释放它指向的所有页。

  1. void pagedir_destroy (uint32_t *pd);

process_exit()

释放当前进程的页目录,并将当前页表切换到init_page_dir,为只有内核可映射的页目录。
注意在切换页表前,必须先将当前进程cur->pagedir设为NULL,否则切换页表后可能被某个中断影响,又切换回去。在释放页表前,必须先切换页表,否则就是释放当前正在使用的页表。

  1. void process_exit (void);

ring

Intel的x86处理器是通过Ring级别来进行访问控制的,CPU运行级别共分4层,RING0, RING1, RING2, RING3
RING0层拥有最高的权限,RING3层拥有最低的权限。按照Intel原有的构想,应用程序工作在RING3层,只能访问RING3层的数据,操作系统工作在RING0层,可以访问所有层的数据,而其他驱动程序位于RING1、RING2层,每一层只能访问本层以及权限更低层的数据。如果普通应用程序企图执行RING0指令,则Windows会显示“非法指令”错误信息。
Windows只使用其中的两个级别:RING0和RING3。

tss_update()

将TSS中的ring 0栈指针,指向当前进程栈的底部。
不是很懂用途,将当前进程栈设为最高优先级?

  1. void tss_update (void);

process_activate()

为当前进程的用户代码设置CPU?
每次上下文切换,都需调用该函数。

  1. void process_activate (void)
  2. {
  3. struct thread *t = thread_current ();
  4. /* 激活线程的页表 */
  5. pagedir_activate (t->pagedir);
  6. /* 设置线程的内核栈,用于处理中断 */
  7. tss_update ();
  8. }

ELF types

用于加载ELF二进制文件的定义。取自ELF手册(ELF specification)。

  1. /* ELF types. See [ELF1] 1-2. */
  2. typedef uint32_t Elf32_Word, Elf32_Addr, Elf32_Off;
  3. typedef uint16_t Elf32_Half;
  4. /* For use with ELF types in printf(). */
  5. #define PE32Wx PRIx32 /* Print Elf32_Word in hexadecimal. */
  6. #define PE32Ax PRIx32 /* Print Elf32_Addr in hexadecimal. */
  7. #define PE32Ox PRIx32 /* Print Elf32_Off in hexadecimal. */
  8. #define PE32Hx PRIx16 /* Print Elf32_Half in hexadecimal. */

Elf32_Ehdr

可执行文件的头部(可执行头部?Executable Header)。在每个ELF二进制文件的最开始都会有。

  1. struct Elf32_Ehdr
  2. {
  3. unsigned char e_ident[16];
  4. Elf32_Half e_type;
  5. Elf32_Half e_machine;
  6. Elf32_Word e_version;
  7. Elf32_Addr e_entry;
  8. Elf32_Off e_phoff;
  9. Elf32_Off e_shoff;
  10. Elf32_Word e_flags;
  11. Elf32_Half e_ehsize;
  12. Elf32_Half e_phentsize;
  13. Elf32_Half e_phnum;
  14. Elf32_Half e_shentsize;
  15. Elf32_Half e_shnum;
  16. Elf32_Half e_shstrndx;
  17. };

Elf32_Phdr

程序的头部(Program Header)。
会有e_phnum个,从文件的e_phoff处开始。

  1. struct Elf32_Phdr
  2. {
  3. Elf32_Word p_type;
  4. Elf32_Off p_offset;
  5. Elf32_Addr p_vaddr;
  6. Elf32_Addr p_paddr;
  7. Elf32_Word p_filesz;
  8. Elf32_Word p_memsz;
  9. Elf32_Word p_flags;
  10. Elf32_Word p_align;
  11. };

p_type, p_flags

p_type, p_flags的可能值。

  1. /* Values for p_type. See [ELF1] 2-3. */
  2. #define PT_NULL 0 /* 忽略 */
  3. #define PT_LOAD 1 /* 可加载段 */
  4. #define PT_DYNAMIC 2 /* 动态链接信息 */
  5. #define PT_INTERP 3 /* 动态加载器的名称 */
  6. #define PT_NOTE 4 /* 辅助信息 */
  7. #define PT_SHLIB 5 /* 保留 */
  8. #define PT_PHDR 6 /* 程序头部表 */
  9. #define PT_STACK 0x6474e551 /* 堆栈段 */
  10. /* Flags for p_flags. See [ELF3] 2-3 and 2-4. */
  11. #define PF_X 1 /* 可执行 */
  12. #define PF_W 2 /* 可写 */
  13. #define PF_R 4 /* 可读 */

load()

file_name加载一个二进制文件到当前进程。并将可执行文件的入口点(Entry Point)保存到EIP中,将可执行文件的初始栈指针保存到ESP中。
如果成功返回true,失败返回false。

函数会依次进行:为其创建、分配、激活新的页目录,打开该二进制文件,读并确认该文件的可执行头部,读该程序的头部,为其创建栈,设置EIP为其入口地址,关闭该文件。

  1. bool load (const char *file_name, void (**eip) (void), void **esp);

validate_segment()

判断一个程序头部phdr是否描述了一个file中有效的、可加载的段。如果是返回true,否则返回false。

phdr描述了一个file中有效的、可加载的段,必须满足以下条件:
phdr的p_offsetp_vaddr必须有相同的页偏移。
phdr必须指向file内部(其p_offset在file内)。
phdr的memsz大小不小于其p_filesz
phdr的p_memsz非空(不为0)。
phdr的虚拟内存区域起始(phdr->p_vaddr)和终止位置(phdr->p_vaddr+phdr->p_memsz)都在用户地址空间内部。
phdr的虚拟内存区域不会超出内核的虚拟地址空间,即phdr->p_vaddr + phdr->p_memsz要大于等于phdr->p_vaddr
不允许映射到页0,即phdr->p_vaddr要大于等于PGSIZE(页0的终止位置)。

  1. static bool
  2. validate_segment (const struct Elf32_Phdr *phdr, struct file *file);

load_segment()

在地址upage处,加载文件file在ofs处的段。
upage开始的read_bytes个字节,会被设为file从ofs开始的对应内容;从upage+read_bytes开始的zero_bytes个字节,会被设为0。
该函数初始化的这read_bytes+zero_bytes个字节,可写性需与writable一致,即writable为true时,这部分需对用户进程可写,否则只可读。
如果上述内容成功实现,返回true,否则内存分配错误或硬盘读写错误,返回false。

函数实现大概为:每次分配一页内存,根据read_byteszero_bytes确定当前页是从file中复制还是设为0。

  1. static bool
  2. load_segment (struct file *file, off_t ofs, uint8_t *upage,
  3. uint32_t read_bytes, uint32_t zero_bytes, bool writable);

setup_stack()

创建一个最小大小(一页)的栈。成功返回true,失败返回false。
通过在用户虚拟空间的最顶部,映射一个清0的页实现。

  1. static bool setup_stack (void **esp);

install_page()

在页表中,添加一个从用户虚拟地址upage到内核虚拟地址kpage的映射。
writable为true,用户进程可以修改对应页,否则对应页对用户只读。
upage必须还未被映射。kpage或许应该是(should probably be?)一个从用户池中获取的页(通过palloc_get_page())。
如果上述内容成功实现,返回true,否则upage已被映射或内存分配失败,返回false。

  1. static bool
  2. install_page (void *upage, void *kpage, bool writable)
  3. {
  4. struct thread *t = thread_current ();
  5. /* 确认对应虚拟地址还没映射到内核页,然后映射 */
  6. return (pagedir_get_page (t->pagedir, upage) == NULL
  7. && pagedir_set_page (t->pagedir, upage, kpage, writable));
  8. }
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注