进程管理
进程作为用户操作的实体,它贯穿操作系统的整个生命周期,而程序是由若干段二进制代码组成的。进程可以说是程序的运行态抽象,即运行于处理器中的二进制码叫做进程,保存在存储介质中的二进制码叫做程序。进程会在执行过程中引入运行环境维护信息,因此进程管理啊主要涉及两个部分内容:进程控制结构体和进程间调度策略。
进程控制结构体用于记录和手机进程运行时的资源消耗信息,并维护程序运行的现场环境;进程间调度策略主要负责决策一个进程将在何时能获得处理器的执行权。
简述进程管理模块
进程作为拥有执行资源的最小单位,他为每个程序维护着运行时的各种资源,比如进程ID、进程的页表、进程执行现场的寄存器值、进程各个段地址空间分布信息以及进程执行时的维护信息等,他们在程序运行期间会被经常或实时更新。这些资源有组织地被结构化到PCB(Process Controller Block,进程控制结构体)内,PCB作为进程调度的决策信息供进程调度算法使用。系统内核的所有组成部分皆是为进程高效、稳定的运行提供服务。
进程调度策略负责将满足运行条件或迫切需要执行的进程调配到空闲处理器中执行。进程调度策略是操作系统中非常重要的一部分,它直接影响程序的执行效率,即使操作系统拥有再强大的处理器,也很可能被糟糕的调度策略拖后腿,甚至拖垮。
CR0-CR3寄存器
CR0中含有控制处理器操作模式和状态的系统控制标志;
CR1保留不用;
CR2含有导致页错误的线性地址;
CR3中含有页目录表物理内存基地址,因此该寄存器也被称为页目录基地址寄存器PDBR(Page-Directory Base addressRegister)。
PCB Process Controller Block 进程控制结构体
用于记录进程的资源使用情况(包括硬件资源和软件资源)和运行态信息等。
struct task_struct
{
struct List list; //双向链表,用于连接各个进程控制结构体
volatile long state;//进程状态:运行态、停止态、可中断态等。
unsigned long flags; //进程标志:进程、线程、内核线程
//volatile 每次使用这个变量前,必须重新读取该变量的值,不能使用寄存器里的备份
struct mm_struct *mm;// 内存空间分布结构体,记录内存页表和程序段信息
struct thread_struct *thread; //负责在进程调度的过程中保存和还原CR3控制寄存器的页目录基地址和通用寄存器值。
unsigned long addr_limit;//进程地址空间范围 0x00000000,00000000 - 0x00007FFF,FFFFFFFF应用层 0xFFFF8000,00000000 - 0xFFFFFFF,FFFFFFFF内核层
long pid; //进程ID号
long counter; // 进程可用时间片
long signal; //进程持有的信号
long priority; //进程优先级
}
struct mm_struct
{
pml4t_t * pgd;//内存页表指针
unsigned long start_code,end_code;//代码段空间
unsigned long start_data,end_data;//数据段空间
unsigned long start_rodata,end_data; //只读数据段空间
unsigned long start_brk,end_brk; //动态内存分配去 (堆区域)
unsigned long start_stack;// 应用层栈基地址
};
struct thread_struct
{
unsigned long rsp0; //内核层栈基地址
unsigned long rip;//内核层代码指针
unsigned long rsp; //内核层当前栈指针
unsigned long fs; //fs 段寄存器
unsigned long gs; // gs 段寄存器
unsigned long cr2;//cr2控制寄存器
unsigned long trap_nr;//产生异常的异常号
unsigned long error_code;//产生错误的错误码
}
借鉴了linux内核的设计思想,即把进程控制结构体struct task_struct与进程的内核层栈空间融为一体。其中,地地址存放struct task_struct 结构体,而余下的地址高地址空间则作为进程的内核层栈空间使用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-15hUgB26-1636897268417)(C:\Users\86187\AppData\Roaming\Typora\typora-user-images\image-20211026225432957.png)]
ld链接脚本
每一个链接过程都由链接脚本(linker script, 一般以lds作为文件的后缀名)控制. 链接脚本主要用于规定如何把输入文件内的section放入输出文件内, 并控制输出文件内各部分在程序地址空间内的布局. 但你也可以用连接命令做一些其他事情.
https://blog.csdn.net/wtbcx2012/article/details/45508113
借助一个联合体,把进程控制结构体struct task_struct与进程的内核层栈空间连续到了一起,其中的宏常量STACK_SIZE被定义为32768B(32KB),他表示进程的内核栈空间和struct task_struct结构体占用的存储空间总量为32KB。
再将union task_union实例化成全局变量init_task_union,并将其作为操作系统的第一个进程。进程控制结构体数组init_task(指针数组)是为了处理器创建的初始进程控制结构体,目前只有数组的第0个元素已投入使用。
拥有进程后如何切换进程?
进程间的切换过程必须要在一块公共区域内进行,故此块区域往往由内核空间提供。这个暗示着进程的应用层空间有应用程序自身维护,从而使得进程可运行格子的程序。而内核空间则用于处理所有进程对系统的操作请求,这其中就包括进程间的切换,因此内核层空间是所有进程共享的。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KqEZtpmt-1636897268419)(C:\Users\86187\AppData\Roaming\Typora\typora-user-images\image-20211028000931552.png)]
prev进程通过调用switch_to模块来保存RSP寄存器的当前值,并指定切换回prev进程时RIP寄存器值,此处默认将其指定在标识符1:处。随后,将next进程的栈指针恢复到RSP寄存器中,再把next进程执行现场的RIP寄存器压入next进程的内核层栈空间(RSP寄存器的恢复前,此后的数据将压入next进程的内核层栈空间)。
其实处理器早就在运行第一个进程中了,只不过此前的进程控制结构体尚未初始化完毕。全局变量中的_stack_start记录着系统第一个进程内核层栈基地址。
通常情况下,系统的第一个进程会协助操作系统完成一些初始化任务,该进程会在执行完初始化任务后进入等待阶段,他会在系统没有可运行进程时休眠处理器以达到省电的目的,因此第一个进程不存在应用层空间。对于这个没用应用层空间的进程而言,其init_mm结构体变量(struct mm_struct结构体)保存的不再是应用程序信息,而是内核程序的各个段信息以及内核层栈基地址。
接下来,为系统创建第二个进程,这个进程常被称作"init进程"。对于调用kernel_thread函数时传入的CLONE_FS、CLONE_FILES、CLONE_SIGNAL等克隆标志位,预留使用。
init函数与主函数一样,经过编译器的编译生成若干个程序片段并记录程序的入口地址,当操作系统为进程创建进程控制结构体时,操作系统会去的程序的入口地址,并从这个入口地址处执行。从编程角度来看,进程是由一系列维护程序运行的信息和若干组片段构成的。
/*
内联函数用来建议编译器对一些特殊的函数进行内联扩展
首先将next进程的内核层栈基地址设置到TSS结构体对应的成员变量中 。虽有保存当前进程的FS与GS段寄存器值,再将next进程保存的FS和GS段寄存器值还原
*/
inline void __switch_to(struct task_struct *prev,struct task_struct *next)
{
init_tss[].rsp0 = next->thread->rsp0;
set_tss64(init_tss[0].rsp0,init_rss[0].rsp1,init_tss[0].rsp2,init_tss[0].ist1,init_tss[0].ist2,init_tss[0].ist3,init_tss[0].ist4,init_tss[o].ist5,init_tss[0].ist6,init_tss[0].ist7);
__asm__ __volatile__(
"movq %%fs, %0 \n\t:"=a"(prev->thread->fs)"
);
__asm__ __volatile__(
"movq %%gs, %0 \n\t":"=a"(prev->thread->gs)
);
__asm__ __volatile__("movq %0, %%fs \n\t"::"a"(next->thread->fs));
__asm__ __volatile__("movq %0, %%gs \n\t"::"a"(next->thread->gs));
color_printk(WHITE,BLACK,"prev->thread->rsp0:%#0181x\n",prev->thread->rsp0);
color_printk(WHITE,BLACK,"next->thread->rsp0:%#0181x\n",next->thread->rsp0);
}
void task_init()
{
struct task_struct *p = NULL;
init_mm.pgd = (pml4t_t *) Global_cr3;
init_mm.start_code = memory_management_struct.start_code;
init_mm.end_code = memory_management_struct.end_code;
init_mm.start_data = (unsigned long)&_data;
init_mm.end_data = memory_management_struct.end_code;
init_mm.start_rodata = (unsigned long)&_rodata;
init_mm.end_rodata = (unsigned long)&_erodata;
init_mm.start_brk = 0;
init_mm.end_brk = memory_management_struct.end_brak;
init_mm.start_stack = _starck_start;
//init_thread,init_tss
set_tss64(init_tss[0].rsp0,init_rss[0].rsp1,init_tss[0].rsp2,init_tss[0].ist1,init_tss[0].ist2,init_tss[0].ist3,init_tss[0].ist4,init_tss[o].ist5,init_tss[0].ist6,init_tss[0].ist7);
init_tss[0].rsp0 = init_thread.rsp0;
list_init(&init_task_union.task.list);
kernel_thread(init,10,CLONE_FS | CLONE_FILES | CLONE_SIGNAL);
init_task_union.task.state = TASK_RUNNING;
p = container_of(list_next(¤t->list),struct task_struct,list);
__switch_to(current,p);
}
unsigned long init(unsigned long arg)
{
color_printk(RED,BLACK,"init task is running ,arg:%#0181x\n",arg);
}
int kernel_thread(unsigned long (* fn)(unsigned long),unsigned long args , unsigned long flags)
{
struct pt_regs regs ;
memset(®s , 0 , sizeof(regs));
regs.rbx = (unsigned long )fn; //保存着程序入口值
regs.rdx = (unsigned long)args;//保存着进程创建者传入的从参数
regs.ds = KERNEL_DS;
regs.es = KERNEL_DS;
regs.cs = KERNEL_CS;
regs.ss = KERNEL_DS;
regs.rflags = (1 << 9);
regs.rip = (unsigned long )kernel_thread_func;//保存着一段引导程序(kernel_thread_func模块),这段引导程序会在目标程序(保存于参数fn内)执行前执行
return do_fork($regs,flags , 0 , 0);//创建进程控制结构体 并完成进程运行前的初始化工作
}
unsigned long do_fork(struct pt_regs * regs, unsigned long clone_flags,unsigned long stack_start,unsigned long stack_size)
{
struct task_struct *tsk = NULL;
struct thread_struct *thd = NULL;
struct Page *p = NULL;
color_printk(WHITE,BLACK,"alloc_pages,bitmap:%#0181x\n",*memory_management_struct.bits_map);
p = alloc_pages(ZONE_NORMAL,1,PG_PTable_Maped | PG_Active | PG_Kernel); // 分配物理页
tsk = (struct task_struct *)Pht_To_Virt(p->PHY_address);
memset(tsk,0,sizeof(*tsk));
*tsk = *current;
list_init(&tsk->list);
list_add_to_before(&init_task_union.task.list,&tsk->list);
tsk->pid ++;
tsk->state = TASK_NINTERRUPTEIBLE;
thd = (struct thread_struct *)(tsk + 1);
tsk ->thread = thd;
memcpy(regs,(void *)((unsigned long)tsk +STACK_SIZE - sizeof(sturct pt_regs)),sizeof(struct pt_regs));
thd->rsp0 = (unsigned long)tsk + STACK_SIZE;
thd->rip = regs->rip;
thd->rsp = (unsigned long)tsk + STACK_SIZE - sizeof(struct pt_regs);
if(!(tsk->flags & PF_KTHREAD))//确定目标进程是在内核层空间还是应用层空间 true 为应用层空间
thd->rip = regs->rip = (unsigned long)ret_from_intr;//若在应用层空间需要将进程的执行入口地址设置在ret_from_intr地址处
//否则的话将进程的入口设置在kernel_thread_func地址处
tsk->state = TASK_RUNNING;
/*
在初始化进程控制结构体是,未曾分配struct mm_struct的存储空间,沿用全局变量init_mm 一单执行switch_to模块,操作系统便会切换进程,从而使处理器执行init进程。由于在init在内核层,因此将会执行kernel_thread_func模块
*/
reutrn 0;
}
/*
当处理器执行kernel_thread_func模块时,栈指针寄存器RSP正指向当前进程的内核层栈顶地址,此刻的栈顶位于栈基地址向下偏移struct pt_regs结构体处。
经过若干个POP汇编指令,最终将RSP寄存器平衡到栈基地址处,进而达到还原进程执行现场的目的。这个现场在kernel_thread中伪造的,其中的RBX寄存器保存着程序执行片段,
RDX寄存器保存着传入的参数。
*/
extern void kernel_thread_func(void);
__asm__ (
"kernel_thread_func: \n\t"
" popq %r15 \n\t"
" popq %r14 \n\t"
" popq %r13 \n\t"
" popq %r12 \n\t"
" popq %r11 \n\t"
" popq %r10 \n\t"
" popq %r9 \n\t"
" popq %r8 \n\t"
" popq %rbx \n\t"
" popq %rcx \n\t"
" popq %rdx \n\t"
" popq %rsi \n\t"
" popq %rdi \n\t"
" popq %rbp \n\t"
" popq %rax \n\t"
" movq %rax, %ds \n\t"
" popq %rax \n\t"
" movq %rax, %es \n\t"
" popq %rax \n\t"
" addq $0x38, %rsp \n\t"
" movq %rax, %rdi \n\t"
" callq *%rbx \n\t"//进程现场还原后,执行CALL寄存器保存的程序执行片段(init进程)
" movq %rax, %rdi \n\t"
" callq %rax, %rdi \n\t"
" callq do_exit \n\t"
)
unsigned long do_exit(unsigned long code)
{
color_printk(RED,BLACK,"exit task is running,arg:%#0181x\n",code);
}
do_fork才是创建进程的核心函数,而kernel_thread函数更像是对创建出的进程做了特殊限制,这个由kernel_thread函数创建出来的进程更像是一个线程。尽管kernel_thread函数借助do_fork函数创建出了进程控制结构体,但是这个进程却没有应用层空间。其实,kernel_thread函数只能创建出没有应用层空间的进程,如果有诸多这样的进程同时运行在内核中,他们看起来更像是内核 主进程创建出的若干个线程一般,因此常被叫做内核线程。综上,kernel_thread的功能是创建内核线程,所以init此时是个内核级线程,但不会一直是个内核线程。当执行do_execve函数后,他会转变为一个用户级线程