本文基于Linux0.11操做系统的源代码,分析其进程模型。html
Linux0.11下载地址:https://zhidao.baidu.com/share/20396e17045cc4ce24058aa43a81bf7b.htmllinux
程序是一个可执行的文件,而进程(process)是一个执行中的程序实例。数组
进程和程序的区别:session
几个进程能够并发的执行一个程序数据结构
一个进程能够顺序的执行几个程序并发
进程由可执行的指令代码、数据和堆栈区组成。进程中的代码和数据部分分别对应一个执行文件中的代码段、数据段。每一个进程只能执行本身的代码和访问本身的数据及堆栈区。进程相互之间的通讯须要经过系统调用来进行。函数
内核程序经过进程表对进程进行管理,每一个进程在进程表中占有一项。在Linux中,进程表项是一个task_struct任务结构指针。学习
任务数据结构定义在头文件 include/linux/sched.h中。或称其为进程控制块PCB(Process Control Block)或进程描述符PD(Processor Descriptor)。spa
其中保存着用于控制和管理进程的全部信息。操作系统
内核程序使用进程标识符(process ID,PID)来标识每一个进程。
struct task_struct { ... pid_t pid;----------进程ID pid_t tgid;---------线程ID ... }
利用分时技术,在Linux操做系统上同时能够运行多个程序。分时技术的基本原理是把CPU的运行时间划分红一个个规定长度的时间片,让每一个进程在一个时间片内运行。
当进程的时间片用完时系统就利用调度程序切换到另外一个程序去运行。所以实际上对于具备单个CPU的机器来讲某一时刻只能运行一个程序。
但因为每一个进程的时间片很短,因此表面看来好像全部进程同时运行着。
一个进程在其说生存期内,可处于一组不一样的状态下,成为进程状态。进程状态保存在进程任务结构的state字段中。
struct task_struct { /* these are hardcoded - don't touch */ long state; /* -1 unrunnable, 0 runnable, >0 stopped */
运行状态(TASK_RUNNING)
当进程正在被CPU执行,或已经准备就绪随时可由调度程序执行,则称该进程为处于运行状态(running)。进程能够在内核态运行,也能够在用户态运行。当系统资源已经可用时,进程就被唤醒而进入准备运行状态,该状态称为就绪态。这些状态(图中中间一列)在内核中表示方法相同,都被成为处于TASK_RUNNING状态。
可中断睡眠状态(TASK_INTERRUPTIBLE)
当进程处于可中断等待状态时,系统不会调度该进行执行。当系统产生一个中断或者释放了进程正在等待的资源,或者进程收到一个信号,均可以唤醒进程转换到就绪状态(运行状态)。
不可中断睡眠状态(TASK_UNINTERRUPTIBLE)
与可中断睡眠状态相似。但处于该状态的进程只有被使用wake_up()函数明确唤醒时才能转换到可运行的就绪状态。
暂停状态(TASK_STOPPED)
当进程收到信号SIGSTOP、SIGTSTP、SIGTTIN或SIGTTOU时就会进入暂停状态。可向其发送SIGCONT信号让进程转换到可运行状态。
僵死状态(TASK_ZOMBIE)
当进程已中止运行,但其父进程尚未询问其状态时,则称该进程处于僵死状态。
当进程的时间片用完时系统就利用调度程序切换到另外一个程序去运行。若是进程在内核态执行时须要等待系统的某个资源,此时该进程就会调用sleep_on()或sleep_on_interruptible()放弃CPU的使用权,进入睡眠状态,调度程序就会去执行其余进程。
extern void sleep_on (struct task_struct **p); // 可中断的等待睡眠。( kernel/sched.c, 167 )
对于Linux0.11内核来说,系统最多可有64个进程同时存在,除了第一个进程是“手工”创建之外,其他的都是进程使用系统调用fork建立的新进程,被建立的进程称为子进程(child process),建立者称为父进程(parent process)。
在boot/目录中引导程序把内核加载到内存中,并让系统进入保护模式下运行后,就开始执行系统初始化程序init/main.c。该程序会进行一些操做使系统各部分处于可运行状态。
此后程序把本身“手工”移动到进程0中运行,并使用fork()调用首次建立出进程1。
“移动到任务0中执行”这个过程由宏move_to_user_mode()include/asm/system.h完成。
//// 切换到用户模式运行。 // 该函数利用iret 指令实现从内核模式切换到用户模式(初始任务0)。 #define move_to_user_mode() \ _asm { \ _asm mov eax,esp /* 保存堆栈指针esp 到eax 寄存器中。*/\ _asm push 00000017h /* 首先将堆栈段选择符(SS)入栈。*/\ _asm push eax /* 而后将保存的堆栈指针值(esp)入栈。*/\ _asm pushfd /* 将标志寄存器(eflags)内容入栈。*/\ _asm push 0000000fh /* 将内核代码段选择符(cs)入栈。*/\ _asm push offset l1 /* 将下面标号l1 的偏移地址(eip)入栈。*/\ _asm iretd /* 执行中断返回指令,则会跳转到下面标号1 处。*/\ _asm l1: mov eax,17h /* 此时开始执行任务0,*/\ _asm mov ds,ax /* 初始化段寄存器指向本局部表的数据段。*/\ _asm mov es,ax \ _asm mov fs,ax \ _asm mov gs,ax \ }
Linux进程是抢占式的。被抢占的进程仍然处于task_running状态,只是暂时没有被CPU运行。进程的抢占发生在进程处于用户态执行阶段,在内核态执行时是不能被抢占的。
为了能让进程有效地使用系统资源,又能使进程有较快的响应时间,就须要对进程的切换调度采用必定的调度策略。在Linux0.11操做系统中采用了基于优先级排队的调度策略。
Schedule()函数首先扫描任务数组。
void schedule (void) { int i, next, c; struct task_struct **p; // 任务结构指针的指针。 /* 检测alarm(进程的报警定时值),唤醒任何已获得信号的可中断任务 */ // 从任务数组中最后一个任务开始检测alarm。 for (p = &LAST_TASK; p > &FIRST_TASK; --p) if (*p) { // 若是任务的alarm 时间已通过期(alarm<jiffies),则在信号位图中置SIGALRM 信号,而后清alarm。 // jiffies 是系统从开机开始算起的滴答数(10ms/滴答)。定义在sched.h 第139 行。 if ((*p)->alarm && (*p)->alarm < jiffies) { (*p)->signal |= (1 << (SIGALRM - 1)); (*p)->alarm = 0; } // 若是信号位图中除被阻塞的信号外还有其它信号,而且任务处于可中断状态,则置任务为就绪状态。 // 其中'~(_BLOCKABLE & (*p)->blocked)'用于忽略被阻塞的信号,但SIGKILL 和SIGSTOP 不能被阻塞。 if (((*p)->signal & ~(_BLOCKABLE & (*p)->blocked)) && (*p)->state == TASK_INTERRUPTIBLE) (*p)->state = TASK_RUNNING; //置为就绪(可执行)状态。 } /* 这里是调度程序的主要部分 */ while (1) { c = -1; next = 0; i = NR_TASKS; p = &task[NR_TASKS]; // 这段代码也是从任务数组的最后一个任务开始循环处理,并跳过不含任务的数组槽。比较每一个就绪 // 状态任务的counter(任务运行时间的递减滴答计数)值,哪个值大,运行时间还不长,next 就 // 指向哪一个的任务号。 while (--i) { if (!*--p) continue; if ((*p)->state == TASK_RUNNING && (*p)->counter > c) c = (*p)->counter, next = i; } // 若是比较得出有counter 值大于0 的结果,则退出124 行开始的循环,执行任务切换(141 行)。 if (c) break; // 不然就根据每一个任务的优先权值,更新每个任务的counter 值,而后回到125 行从新比较。 // counter 值的计算方式为counter = counter /2 + priority。[右边counter=0??] for (p = &LAST_TASK; p > &FIRST_TASK; --p) if (*p) (*p)->counter = ((*p)->counter >> 1) + (*p)->priority; } switch_to (next); // 切换到任务号为next 的任务,并运行之。 }
经过比较每一个就绪态(task_running)任务的运行时间递减计数counter的值来肯定哪一个进程运行的时间最少,选择该进程运行。
若是此时全部处于task_running状态进程的时间片都已经用完,系统就会根据进程的优先权值priority,对系统中全部(包括正在睡眠)进程从新计算每一个任务须要运行的时间片值counter。
计算的公式是
而后schedule()函数从新扫描任务数组中全部处于task_running状态任务,重复上述过程,直到选择出一个进程为止。
最后调用switch_to()执行实际的进程切换操做。若是此时没有其它进程可运行,系统就会选择进程0运行。
当一个进程结束了运行或者在半途中终止了运行,那么内核就须要释放改进程所占用的系统资源。
用户程序调用exit()系统调用时,执行内核函数do_exit()。
//// 程序退出处理程序。在系统调用的中断处理程序中被调用。 int do_exit (long code) // code 是错误码。 { int i; // 释放当前进程代码段和数据段所占的内存页(free_page_tables()在mm/memory.c,105 行)。 free_page_tables (get_base (current->ldt[1]), get_limit (0x0f)); free_page_tables (get_base (current->ldt[2]), get_limit (0x17)); // 若是当前进程有子进程,就将子进程的father 置为1(其父进程改成进程1)。若是该子进程已经 // 处于僵死(ZOMBIE)状态,则向进程1 发送子进程终止信号SIGCHLD。 for (i = 0; i < NR_TASKS; i++) if (task[i] && task[i]->father == current->pid) { task[i]->father = 1; if (task[i]->state == TASK_ZOMBIE) /* assumption task[1] is always init */ (void) send_sig (SIGCHLD, task[1], 1); } // 关闭当前进程打开着的全部文件。 for (i = 0; i < NR_OPEN; i++) if (current->filp[i]) sys_close (i); // 对当前进程工做目录pwd、根目录root 以及运行程序的i 节点进行同步操做,并分别置空。 iput (current->pwd); current->pwd = NULL; iput (current->root); current->root = NULL; iput (current->executable); current->executable = NULL; // 若是当前进程是领头(leader)进程而且其有控制的终端,则释放该终端。 if (current->leader && current->tty >= 0) tty_table[current->tty].pgrp = 0; // 若是当前进程上次使用过协处理器,则将last_task_used_math 置空。 if (last_task_used_math == current) last_task_used_math = NULL; // 若是当前进程是leader 进程,则终止全部相关进程。 if (current->leader) kill_session (); // 把当前进程置为僵死状态,并设置退出码。 current->state = TASK_ZOMBIE; current->exit_code = code; // 通知父进程,也即向父进程发送信号SIGCHLD -- 子进程将中止或终止。 tell_father (current->father); schedule (); // 从新调度进程的运行。 return (-1); /* just to suppress warnings */ }
若是进程有子进程,则让init进程做为其因此子进程的父进程。
而后把进程状态设置为僵死状态task_zombie。并向其原父进程发送SIGCHLD信号,通知其某个子进程已经终止。
在进程被终止是,它的任务数据结构仍然保留着。由于其父进程还须要使用其中的信息。
在子进程执行期间,父进程一般使用wait()或waitpid()函数等待某个子进程终止。
当等待的子进程被终止并处于僵死状态时,父进程就会把子进程运行所使用的时间累加到本身进程中,释放子进程任务数据结构。
正如Linux系统创始人在一篇新闻组投稿上所说的,要理解一个软件系统的真正运行机制,必定要阅读其源代码。
但因为目前Linux内核整个源代码的大小已经很是得大(例如2.2.20版具备268万行代码!!)因此本文基于Linux0.11操做系统的源代码,分析其进程模型。
虽然所选择的版本较低,各方面都有很大的提高空间,但该内核已可以正常编译运行,其中已经包括了Linux工做原理的精髓,与目前Linux内核基本功能较为相近,源代码又很是短小精干,所以会有极高的学习效率,可以作到事半功倍,快速入门。