作业内容:
挑选一个开源的操作系统,深入源码分析其进程模型,具体包含如下内容:
进程的创建:当启动linux时,加载完内核后,会创建第一进程init。之后所有的进程都是由init或父进程向内核发起调用fork()自身而来,再而克隆clone()自身的数据给子进程。
进程的优先级:系统中同时有大量的进程需要执行时,是需要优先级来先后处理的。task_struct中是有记录优先级的,内核是如何知道哪个优先级高哪个低,遍历。内核2.6将进程优先级划分位固定的个数。
Linux环境下分别使用getpid()和getppid()函数来得到进程ID和父进程ID,分别使用getuid()和geteuid()函数来得到进程的用户ID和有效用户ID,分别使用getgid()和getegid()来获得进程的组ID和有效组ID,其函数原型如下:
#include 3.操作系统是怎么组织进程的 task_struct的 real_parent成员指向父进程,parent成员指向“养父进程”;children成员表示该进程的子进程链表;sibling成员表示该进程的兄弟进程链表。系统启动后创建了第一个进程:进程0(swapper,也叫idle),进程0创建了第一个用户进程(进程1/sbin/init)和第一个内核进程(进程2kthreadd),之后进程0进入idle状态,当没有进程可以被调度的时候运行该进程,不做具体的事情。进程1的主要作用是处理僵尸进程。当某个父进程比子进程提前消亡时,父进程会给子进程重新寻找“养父进程”,一般就是进程1,由进程1负责处理该子进程的消亡。当创建一个新进程时,新进程的父进程为当前进程或者线程,并且把新进程加入到父进程的子链表中。如果使用CLONE_THREAD标志创建一个新进程时,新进程的父进程为当前进程或线程的父进程,并且把新进程加入到父进程的子链表中。当CLONE_THREAD标志创建一个新线程时,新线程的父进程为当前线程组长的父进程,所以线程组看起来像是一个独立的进程。此时,因为创建的是线程,所以不需要把新线程加入到任何子链表中。 由于进程的不断创建,系统的资源已经不能满足进程运行的要求,这个时候就必须把某些进程挂起(suspend),对换到磁盘镜像区中,暂时不参与进程调度,起到平滑系统操作负荷的目的。 引起进程状态转换的具体原因如下: Linuz 中进程调试都是非强占式的,系统采用相当简单的基于优先级的调试算法。进程只能在核心态等待,当进程执行系统调用时它会从用户态“陷入”核心态,这时内核代表这个进程执行。当进程在核心态等待某事件时系统会将它挂起,而后让另一个进程运行。Liunz 的调度函数是schedule,定义在kernelfsched.c 中。Schedule 函数完成以下工作: (1)定义两个指针。prev 是正在运行的进程,next 是调度程序选择下一个将要运行的进程。这两个指针可能会相同。 (2) 处理调度任务队列。首先检查任务是否在队列中,如果有等待处理的任务则顺序处理其中的每一个任务,然后将队列清空。 (3) 检查任务是否在中断队列中,如果在,则不做本次调度,直接返回。 (4) 底半处理。就是中断处理的后半部分。 (5) 调度运行进程队列。与Minix 不同的是Linuz 没有将可运行进程按其优先级分成多个队列,而是把系统中所有可运行进程(状态为TASK_RUNNING) 都排在同一个队列中,这实际上是一个双向链表。 (6) 选择下一个要运行的进程。从init_task.nezt开始,顺序搜索运行进程队列,对每一个进程计算其weight 值,选择weight 值最大的运行。weight 值的计算规则是: 1.如果当前进程声明要放弃CPU (进程调度策略中设置了SCHED_YIEID 位),则其weigt=0。2.如果进程是实时进程,则weight=rt_priority+1000。3.如果进程的counter’值为0,则weight=0。4.如果进程的counter不为0,则weight=countertpriority; 如果进程是当前进程,则其weight 值再加1,即当前进程优先。 1.创建一些局部变量 struct task_struct *prev, *next;//当前进程和一下个进程的进程结构体 2.关闭内核抢占,初始化一部分变量 need_resched: 3.选择next进程 next = pick_next_task(rq, prev);//挑选一个优先级最高的任务排进队列 4.完成进程的调度 if (likely(prev != next)) { static inline void 5.我们看到在context_switch中使用switch_to(prev,next,prev)来切换进程。我们查看一下switch_to的代码。 switch_to是一个宏定义,完成进程从prev到next的切换,首先保存flags,然后保存当前进程的ebp,然后把当前进程的esp保存到prev->thread.sp中,然后把标号1:的地址保存到prev->thread.ip中。 然后把next->thread.ip压入堆栈。这里,如果之前B也被switch_to出去过,那么next->thread.ip里存的就是下面这个1f的标号,但如果next进程刚刚被创建,之前没有被switch_to出去过,那么next->thread.ip里存的将是ret_ftom_fork __switch_canqry应该是现代操作系统防止栈溢出攻击的金丝雀技术。 jmp __switch_to使用regparm call, 参数不是压入堆栈,而是使用寄存器传值,来调用__switch_to eax存放prev,edx存放next。这里为什么不用call __switch_to而用jmp,因为call会导致自动把下面这句话的地址(也就是1:)压栈,然后__switch_to()就必然只能re到这里,而无法根据需要ret到ret_from_fork 当一个进程再次被调度时,会从1:开始执行,把ebp弹出,然后把flags弹出。 #define switch_to(prev, next, last) \ 5.开始抢占 sched_preempt_enable_no_resched(); 转:https://www.cnblogs.com/1014221651a/p/9008427.html
pid_t getpid(void); //获取进程ID
pid_t getppid(void); //获取父进程ID
uid_t getuid(void); //获取用户ID
uid_t geteuid(void); //获取有效用户ID
gid_t getgid(void); //获取组ID
gid_t getegid(void); //获取有效组ID2.查看进程
4.进程状态如何转换
引起进程挂起的原因是多样的,主要有:
1. 系统中的进程均处于等待状态,处理器空闲,此时需要把一些阻塞进程对换出去,以腾出足够的内存装入就绪进程运行。
2. 进程竞争资源,导致系统资源不足,负荷过重,此时需要挂起部分进程以调整系统负荷 ,保证系统的实时性或让系统正常运行。
3. 把一些定期执行的进程(如审计程序、监控程序、记账程序)对换出去,以减轻系统负荷。
4. 用户要求挂起自己的进程,以便根据中间执行情况和中间结果进行某些调试、检查和改正。
5. 父进程要求挂起自己的后代进程,以进行某些检查和改正。
6. 操作系统需要挂起某些进程,检查运行中资源使用情况,以改善系统性能;或当系统出现故障或某些功能受到破坏时,需要挂起某些进程以排除故障。5.进程是如何调度的
(7) 切换。保存当前进程结构(task_struct )中的上下文,就是当前进程运行到调度程序结束时处理器的上下文。新加载进程的上下文也是它上次运行到调度地的快照,包括进程的程序计数器和各寄存器的内容。
unsigned long *switch_count;//进程切换次数
struct rq *rq;//就绪队列
int cpu;
preempt_disable();//关闭内核抢占
cpu = smp_processor_id();
rq = cpu_rq(cpu);//与CPU相关的runqueue保存在rq中
rcu_note_context_switch(cpu);
prev = rq->curr;//将runqueue当前的值赋给prev
clear_tsk_need_resched(prev);//清除prev的TIF_NEED_RESCHED标志。
clear_preempt_need_resched();
//如果prev和next是不同进程
rq->nr_switches++;//队列切换次数更新
rq->curr = next;
++*switch_count;//进程切换次数更新
context_switch(rq, prev, next); /* unlocks the rq *///进程上下文的切换
/*
* The context switch have flipped the stack from under us
* and restored the local variables which were saved when
* this task called schedule() in the past. prev == current
* is still correct, but it can be moved to another cpu/rq.
*/
cpu = smp_processor_id();
rq = cpu_rq(cpu);
} else//如果是同一个进程不需要切换
raw_spin_unlock_irq(&rq->lock);
context_switch(struct rq *rq, struct task_struct *prev,
struct task_struct *next)
{
struct mm_struct *mm, *oldmm;//初始化进程地址管理结构体mm和oldmm
prepare_task_switch(rq, prev, next);//完成进程切换的准备工作
mm = next->mm;
oldmm = prev->active_mm;
/*完成mm_struct的切换*/
if (!mm) {
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
enter_lazy_tlb(oldmm, next);
} else
switch_mm(oldmm, mm, next);
if (!prev->mm) {
prev->active_mm = NULL;
rq->prev_mm = oldmm;
}
switch_to(prev, next, prev);//进程切换的核心代码
barrier();
finish_task_switch(this_rq(), prev);
}
do { \
/* \
* Context-switching clobbers all registers, so we clobber \
* them explicitly, via unused output variables. \
* (EAX and EBP is not listed because EBP is saved/restored \
* explicitly for wchan access and EAX is the return value of \
* __switch_to()) \
*/ \
unsigned long ebx, ecx, edx, esi, edi; \
\
asm volatile("pushfl\n\t" /* save flags */ \
"pushl %%ebp\n\t" /* save EBP */ \
"movl %%esp,%[prev_sp]\n\t" /* save ESP */ \
"movl %[next_sp],%%esp\n\t" /* restore ESP */ \
"movl $1f,%[prev_ip]\n\t" /* save EIP */ \
"pushl %[next_ip]\n\t" /* restore EIP */ \
__switch_canary \
"jmp __switch_to\n" /* regparm call */ \
"1:\t" \
"popl %%ebp\n\t" /* restore EBP */ \
"popfl\n" /* restore flags */ \
\
/* output parameters */ \
: [prev_sp] "=m" (prev->thread.sp), \
[prev_ip] "=m" (prev->thread.ip), \
"=a" (last), \
\
/* clobbered output registers: */ \
"=b" (ebx), "=c" (ecx), "=d" (edx), \
"=S" (esi), "=D" (edi) \
\
__switch_canary_oparam \
\
/* input parameters: */ \
: [next_sp] "m" (next->thread.sp), \
[next_ip] "m" (next->thread.ip), \
\
/* regparm parameters for __switch_to(): */ \
[prev] "a" (prev), \
[next] "d" (next) \
\
__switch_canary_iparam \
\
: /* reloaded segment registers */ \
"memory"); \
} while (0)
if (need_resched())
goto need_resched;参考文献:https://www.cnblogs.com/xiaomanon/p/4195327.html
http://dict.youdao.com/w/%E8%BF%9B%E7%A8%8B%E6%A0%87%E8%AF%86%E7%AC%A6/