本书是上一章的续集。在上一本书中,我们提到操作系统通过伪造中断并通过move_to_user_mode方法从中断返回,巧妙地从内核态切换到用户态。voidmain(void){...move_to_user_mode();如果(!fork()){初始化();for(;;)pause();}今天又要说说fork了。但是这个是创建新进程的过程,是可以体现操作系统设计的地方。所以先不急着看代码,今天来头脑风暴一下,就是如果让你来设计整个流程调度,你会怎么做?别跟我说你先设计锁,volatile等等,这不是进程调度本身最关心的问题。进程调度的本质是什么?很简单,假设将三段代码加载到内存中。进程调度就是让CPU在程序1的位置运行一会儿,在程序2的位置运行一会儿。好吧,就这么简单,不要反驳我,继续往下看。整体的流程设计怎么才能做到刚才说的跑来跑去呢?第一种方式是在程序1的代码中每隔几行写一段代码,主动放弃自己的执行权。跳转到程序2运行的地方。那么程序2也是如此。但是这种靠程序本身的方法肯定是不靠谱的。因此,第二种方法是每隔一段时间,由于不受任何程序控制的第三方不可抗力,中断CPU的运行,然后跳转到专门的程序。CPU接下来要运行的程序的地址,然后跳转到它。这种每隔一段时间中断一次CPU的不可抗力就是定时器触发的时钟中断。不知道大家还记不记得,这个定时器和时钟中断是在第十八章讲到的sched_init函数中完成的|著名的进程调度就从这里开始。而那个特殊的程序就是具体的进程调度函数。那么整个过程就是这样处理的,那么应该设计什么样的数据结构来支持这个过程呢?假设这个结构称为tast_struct。structtask_struct{?}也就是说,你必须有一个结构体来记录每个进程的信息,比如上次执行到哪里,或者CPU决定跳转到你的进程上运行,具体跳到哪一行运行,一定要有地方保存吗?让我们一一来看问题。上下文每个程序的最终本质是执行指令。这个过程涉及到寄存器、内存和外设端口。内存也可以设计成相互错开,互不干扰。比如你给进程1使用0~1K的内存空间,给进程2使用1K~2K的内存空间,不要影响其他人。虽然有点浪费篇幅,对程序员也很不友好,但至少可以实现。但是,寄存器一共就这么多,要避免互相干扰是肯定不行的。也许一个进程使用了??所有的寄存器,那么其他进程会发生什么。例如,程序1刚刚向eax中写入了一个值并准备使用它。这时,它切换到进程2,再次向eax写入一个值。然后后面切换回进程1的时候,就报错了。所以最稳妥的办法就是每次切换进程的时候把这些寄存器的当前值保存在一个地方,这样以后切换回来的时候可以恢复。Linux0.11做到了这一点。在每个进程的结构体task_struct中,都有一个叫做tss的结构体,里面存放着CPU的这些寄存器的信息。structtask_struct{...structtss_structtss;}structtss_struct{longback_link;/*高16位零*/longesp0;长ss0;/*高16位零*/longesp1;长ss1;/*高16位零*/longesp2;长ss2;/*高16位零*/longcr3;长艾普;长旗帜;长eax、ecx、edx、ebx;特别长;长ebp;长埃斯;长编辑;长es;/*高16位零*/longcs;/*高16位为零*/longss;/*高16位零*/longds;/*高16位零*/longfs;/*高16位零*/longgs;/*高16位零*/longldt;/*高16位零*/longtrace_bitmap;/*位:跟踪0,位图16-31*/structi387_structi387;};.有没有发现tss结构中有一个cr3?表示cr3寄存器中存放的值,cr3寄存器指向页目录表的头地址。那么指向不同的页目录表,整个页表结构就是一个完全不同的集合,所以逻辑地址和线性地址的映射关系可以不同。也就是说,在我们刚才假设的理想情况下,不同的程序可以使用不同的内存地址,这样内存就不会互相干扰了。但是有了这个cr3域,就没有必要让每个进程都保证不会和其他进程使用的内存冲突了,因为它只需要建立不同的映射关系,操作系统会建立不同的页目录表并替换cr3寄存器。能。这也可以理解为保存内存映射的上下文信息。当然,Linux0.11并没有通过替换cr3寄存器来实现内存相互干扰。它的实现比较简单,这是后话。运行时间信息如何判断一个进程应该让出CPU,切换到下一个进程呢?总不能每次时钟中断都切换吧?一来不灵活,二来完全依赖时钟中断的频率,有点Danger。所以一个好的方法就是给进程一个属性,叫做剩余时间片。每次时钟中断到来,都会为-1。如果减小到0,就会触发切换进程的操作。在Linux0.11中,这个属性是counter。structtask_struct{...长计数器;...structtss_structtss;}而且它的用法也很简单,就是每次打断判断是否到0。voiddo_timer(longcpl){...//当前线程还有剩余时间片,直接返回if((--current->counter)>0)return;//如果没有剩余时间片,schedule();}如果还没有到0,则直接返回,也就是说这次时钟中断什么也没做,只是把当前进程的时间片属性设置为-1。如果已经到0,触发进程调度,选择下一个进程,让CPU跳到那里运行。进程调度的逻辑在schedule函数中,我们不关心怎么调。优先级之上的计数器一开始应该是多少?而随着计数器的不断递减,当它达到0时,下一个周期应该给计数器赋什么值呢?其实这两个问题是同一个问题,就是计数器的初始化问题也需要有一个属性来记录这个值。宏观上想一下,这个值越大,计数器就越大,每次轮到这个进程,它在CPU中运行的时间就越长,也就是这个进程比其他进程获得更多的CPU算力。时间。那么我们可以把这个值称为优先级,是不是很形象。structtask_struct{...长计数器;长期优先;...structtss_structtss;}每次进程初始化时,计数器都会被赋值这个优先级,当计数器减为0时,下一次分配时间片也是这样赋值。其实叫什么都可以,反正就是这么用的,所以叫优先级。进程状态其实有了以上三个信息,我们就已经可以完成进程调度了。即使你的操作系统允许所有进程获得相同的运行时间,甚至不需要记录计数器和优先级,操作系统本身设置了一个固定值并不断减少。当它达到0时,将随机切割一个新进程。这样就只维护了寄存器的上下文信息tss。但是我们总是要不断优化以满足用户在不同场景下的需求,所以再优化一个细节。一个很简单的场景,在一个进程中有一个读取硬盘的操作。发起读请求后,需要很长时间才能得到硬盘的中断信号。这个时候,进程占用CPU是没有用的。这时候你可以选择主动放弃CPU执行权,然后将自己的状态标记为waiting。意思是告诉进程调度代码,先不要调度我,因为我还在等硬盘中断,现在轮到我也没用了,还是给别人机会吧。那么这个state可以记录一个属性,叫做state,这个属性记录了进程此时的状态。structtask_struct{长状态;长柜台;长期优先;...structtss_structtss;}在Linux0.11中这个进程有五种状态。#defineTASK_RUNNING0#defineTASK_INTERRUPTIBLE1#defineTASK_UNINTERRUPTIBLE2#defineTASK_ZOMBIE3#defineTASK_STOPPED4好了,现在我们可以用这几个字段完成简单的进程调度任务了。有state代表状态,counter代表剩余时间片,priority代表优先级,tss代表上下文信息。其他字段需要用到的时候再说。今天只是集思广益的流程调度设计思路。让我们看一下Linux0.11中的整个进程结构,心里有个数。不要担心具体细节,只要记住我们刚刚集思广益的四个领域即可。structtask_struct{/*这些是硬编码的——不要碰*/longstate;/*-1不可运行,0可运行,>0已停止*/longcounter;长期优先;长信号;结构sigactionsigaction[32];长期封锁;/*屏蔽信号的位图*//*各种字段*/intexit_code;unsignedlongstart_code、end_code、end_data、brk、start_stack;longpid,father,pgrp,session,leader;无符号短uid、euid、suid;无符号短gid,egid,sgid;长报警;longutime,stime,cutime,cstime,start_time;unsignedshortused_math;/*文件系统信息*/inttty;/*-1如果没有tty,那么它必须被签名*/unsignedshortumask;结构m_inode*pwd;结构m_inode*根;结构m_inode*可执行文件;无符号长close_on_exec;structfile*filp[NR_OPEN];/*ldtforthistask0-zero1-cs2-ds&ss*/structdesc_structldt[3];/*tssforthistask*/structtss_structtss;};看吧,其实也没有多少咯~好啦,今天我们进程调度的大体过程和它需要的数据结构完全是我们自己从头设计的。我们知道进程调度的开始需要从一个timertick触发,然后通过时钟中断处理函数去执行进程调度函数。然后到进程结构体task_struct中取出需要的数据,进行策略计算,选择下一个可以让CPU运行的进程,跳转到它。那么下一讲,我们就从一个时钟中断开始,看一下Linux0.11进程调度的整个过程。有了这两个铺垫,再看主进程中的fork代码,就很清楚了!想知道接下来会发生什么,就听下一章吧。本文转载自微信公众号《低并发编程》,可通过以下二维码关注。转载本文请联系低并发编程公众号。
