Linux操作系统原理与应用(操作系统原理之Linux进程调度和管理)
X86架构
CPU包括3个部分:运算单元,数据单元,控制单元
总线上主要有两类数据,一类是地址数据(要拿内存中哪个位置的数据),这类总线是地址总线;另一类是真正的数据,这类总线是数据总线
32位CPU包含的寄存器
- 通用寄存器:EAX/EBX/ECX/EDX/ESI/EDI/ESP/EBP
- 段寄存器:CS/DS/ES/FS/GS/SS
- 指令寄存器:EIP
- 标志寄存器:EFLAGS
- 控制寄存器:CR0/CR2/CR3/CR4
- 系统表指针寄存器:IDTR/GDTR/LDTR
- 任务寄存器:TR
- 调试寄存器:DR0-DR7
PS:视频相关学习文档,后台私信(Linux)
- CS:存储代码段的基址
- DS:存储数据段的基址
- SS:存储运行栈的基址
- EBP:栈基指针寄存器
- ESP:栈顶指针寄存器
- IP:指令指针寄存器,将CS:IP获取到的指令存储EIP(指令寄存器)中
操作系统启动流程
主板上的ROM固化了一段初始化程序BIOS(Basic Input and Output System,基本输入输出系统)
Grub2是一个linux启动管理器 ,Grub2把boot.img共512字节安装到启动盘的第一个扇区,这个扇区称为MBR(Master Boot Record,主引导记录扇区)
- 主机上电后,CS重置为0xFFFF,IP重置为0x0000,所以第一条指令指向0xFFFF0,正好在ROM范围内,从这里开始进行硬件检查
- 将磁盘引导扇区读入0x7c00处,并从0x7c000开始执行
- boot模块:bootsect.s,继续读取其它扇区
- setup模块:setup.s
- 从实模式切换到保护模式(CR0寄存器最后一位置1)
- 初始化GDT,IDT
- system模块:许多文件(head.s,main.c ...)
- 进入main函数,进入各种初始化操作 INIT_TASK(init_task):系统创建第一个进程,0号进程。这是唯一一个没有通过fork或者kernel_thread产生的进程,是进程列表的第一个 trap_init() mm_init() sched_init() rest_init()
- kernel_thread(kernel_init, NULL, CLONE_FS) 创建第二个进程,这个是1号进程。 1号进程即将运行一个用户进程
- 从内核态到用户态
- ramdisk
- 创建2号进程
0号进程 -> 1号内核进程 -> 1号用户进程(init进程) -> getty进程 -> shell进程 -> 命令行执行进程。
- 加载内核:打开电源后,加载BIOS(只读,在主板的ROM里),硬件自检,读取MBR(主引导记录),运行Boot Loader,内核就启动了,这里还有从实模式到保护模式的切换
- 进程:内核启动后,先启动第0号进程init_task,然后在内核运行init()函数,1号进程进入用户态变为init进程,init进程是所有用户态进程的老祖宗,其配置文件是/etc/inittab,通过该文件设置运行等级:根据/etc/inittab文件设置运行等级,比如有无网络
- 系统初始化:启动第一个用户层文件/etc/rc.d/rc.sysinit,然后激活交换分区、检查磁盘、加载硬件模块等
- 建立终端:系统初始化后返回init,这时守护进程也启动了,init接下来打开终端以供用户登录
- init_task是第0号进程,唯一一个没有通过fork或kernel_thread产生的进程,是进程列表的第一个
- init是第1号进程,会成为用户态,是所有用户态进程的祖先
- kthreadd是第2号进程,会成为内核态,是所有内核态进程的祖先
系统调用
int指令将CS中的CPL改为0,从而进入内核,这是用户程序发起调用内核代码的唯一方式
系统调用核心
- 用户程序中包含一段包含int指令的代码
- 操作系统写中断处理,获取调用程序的编号
- 操作系统根据编号执行相应代码
- 在glibc里,任何一个系统调用都会调用DO_CALL,在DO_CALL里会根据系统调用名称,得到系统调用号,放在寄存器EAX里,把请求参数放在其它寄存器里,然后执行int $0x80
- int $0x80触发一个软中断,通过它陷入内核 内核启动时trap_init()指定软中断陷入门的入口,即entry_INT80_32
- 保存当前用户态的寄存器,保存在pt_regs结构里
- 将系统调用号从寄存器EAX里取出来,然后根据系统调用号,在系统调用表中找到相应的函数进行调用
- 系统调用结束后, 原来用户态保存的现场恢复回来
进程状态切换
- 就绪状态(ready):等待被调度
- 运行状态(running)
- 阻塞状态(waiting):等待资源
- 只有就绪态和运行态可以相互转换,其它的都是单向转换。就绪状态的进程通过调度算法从而获得 CPU 时间,转为运行状态;而运行状态的进程,在分配给它的 CPU 时间片用完之后就会转为就绪状态,等待下一次调度。
- 阻塞状态是缺少需要的资源从而由运行状态转换而来,但是该资源不包括 CPU 时间,缺少 CPU 时间会从运行态转换为就绪态。
进程数据结构
Linux的PCB是task_struct结构
进程状态
很多操作系统教科书将正在CPU上执行的进程定义为RUNNING状态,而将可执行但是尚未被调度执行的进程定义为READY状态,这两种状态在linux下统一为TASK_RUNNING状态
任务ID
在Linux系统中,一个线程组中的所有线程使用和该线程组的领头线程相同的pid,并被存放在tgid成员中。只有线程组的领头线程的pid成员才会被设置为与tgid相同的值
注意,getpid()系统调用返回的是当前进程的tgid值而不是pid值
用户栈和内核栈
内核在创建进程的时候,在创建task_struct的同时,会为进程创建相应的堆栈。每一个进程都有两个栈,一个用户栈,存在于用户空间;一个内核栈,存在于内核空间
当进程在用户空间运行时,CPU堆栈指针寄存器里面的内容都是用户栈地址,使用用户栈;当进程在内核空间运行时,CPU堆栈指针寄存器里面的内容是内核栈地址,使用内核栈
当进程因为中断或者系统调用陷入到内核态时,进程所使用的堆栈也要从用户栈转到内核栈。
进程陷入到内核态后,先把用户态堆栈的地址保存在内核栈之中,然后设置堆栈指针寄存器的内容为内核栈的地址,这样就完成了用户栈向内核栈的转换;当进程从内核态恢复到用户态时,在内核态之后的最后将保存在内核栈里面的用户栈的地址恢复到堆栈指针寄存器即可。这样就实现了内核栈向用户栈的转换
在进程从用户态转到内核态的时候,进程的内核栈总是空的。这是因为当进程在用户态运行时使用用户栈,当进程陷入到内核态时,内核保存进程在内核态运行的相关信息,但是一旦进程返回到用户态后,内核栈中保存的信息全部无效,因此每次进程从用户态陷入内核的时候得到的内核栈都是空的。所以在进程陷入内核的时候,直接把内核栈的栈顶地址给堆栈指针寄存器就可以了
task_struct->stack指向进程的内核栈,大小为8K
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};
整个内核栈用union表示,thread_info和stack共用一段存储空间,thread_info占用低地址。在pt_regs和STACK_END_MAGIC之间,就是内核代码的运行栈。当内核栈增长超过STACK_END_MAGIC就会报内核栈溢出
thread_info:存储内核态运行的一些信息,如指向task_struct的task指针,使得陷入内核态之后仍然能够找到当前进程的task_struct,还包括是否允许内核中断的preemt_count开关等等
pt_regs:存储用户态的硬件上下文,用户态进入内核态后,由于使用的栈、内存地址空间、代码段等都不同,所以用户态的eip、esp、ebp等需要保存现场,内核态恢复到用户态时再将栈中的信息恢复到硬件。由于进程调度一定会在内核态的schedule函数,用户态的所有硬件信息都保存在pt_regs中了。SAVE_ALL指令就是将用户态的cpu寄存器值保存如内核栈,RESTORE_ALL就是将pt_regs中的值恢复到寄存器中,这两个指令在介绍中断的时候还会提到
TSS(task state segment):这是intel为上层做进程切换提供的硬件支持,还有一个TR(task register)寄存器专门指向这个内存区域。当TR指针值变更时,intel会将当前所有寄存器值存放到当前进程的tss中,然后再讲切换进程的目标tss值加载后寄存器中
这里很多人都会有疑问,不是有内核栈的pt_regs存储硬件上下文了吗,为什么还要有tss?前文说过,进程切换都是在内核态,而pt_regs是保存的用户态的硬件上下文,tss用于保存内核态的硬件上下文
但是linux并没有买账使用tss,因为linux实现进程切换时并不需要所有寄存器都切换一次,如果使用tr去切换tss就必须切换全部寄存器,性能开销会很高。这也是intel设计的败笔,没有把这个功能做的更加的开放导致linux没有用。linux使用的是软切换,主要使用thread_struct,tss仅使用esp0这个值,用于进程在用户态 -> 内核态时,硬件会自动将该值填充到esp寄存器。在初始化时仅为每1个cpu仅绑定一个tss,然后tr指针一直指向这个tss,永不切换。
thread_struct:一个和硬件体系强相关的结构体,用来存储内核态切换时的硬件上下文
context_switch执行过程
内存空间切换
将curr_task设置为新进程的task
将cpu寄存器保存到旧进程的thread_struct结构
将新进程的thread_struct的寄存器的值写入cpu
切换栈顶指针
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 lqy2005888@qq.com 举报,一经查实,本站将立刻删除。