1.前言
本文基于Linux0.11操作系统的源代码,分析其进程模型。
Linux0.11下载地址:https://zhidao.baidu.com/share/20396e17045cc4ce24058aa43a81bf7b.html
2.进程的定义
程序是一个可执行的文件,而进程(process)是一个执行中的程序实例。
进程和程序的区别:
几个进程可以并发的执行一个程序
一个进程可以顺序的执行几个程序
进程由可执行的指令代码、数据和堆栈区组成。进程中的代码和数据部分分别对应一个执行文件中的代码段、数据段。每个进程只能执行自己的代码和访问自己的数据及堆栈区。进程相互之间的通信需要通过系统调用来进行。
2.1任务数据结构
内核程序通过进程表对进程进行管理,每个进程在进程表中占有一项。在Linux中,进程表项是一个task_struct任务结构指针。
任务数据结构定义在头文件 include/linux/sched.h中。或称其为进程控制块PCB(Process Control Block)或进程描述符PD(Processor Descriptor)。
其中保存着用于控制和管理进程的所有信息。
2.2进程标识符
内核程序使用进程标识符(process ID,PID)来标识每个进程。
struct task_struct { ... pid_t pid;----------进程ID pid_t tgid;---------线程ID ... }
3.进程运行状态
3.1分时技术
利用分时技术,在Linux操作系统上同时可以运行多个程序。分时技术的基本原理是把CPU的运行时间划分成一个个规定长度的时间片,让每个进程在一个时间片内运行。
当进程的时间片用完时系统就利用调度程序切换到另一个程序去运行。因此实际上对于具有单个CPU的机器来说某一时刻只能运行一个程序。
但由于每个进程的时间片很短,所以表面看来好像所有进程同时运行着。
3.2进程状态
一个进程在其说生存期内,可处于一组不同的状态下,成为进程状态。进程状态保存在进程任务结构的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 )
3.3进程初始化
对于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 }
4.进程调度
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运行。
5.终止进程
当一个进程结束了运行或者在半途中终止了运行,那么内核就需要释放改进程所占用的系统资源。
用户程序调用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()函数等待某个子进程终止。
当等待的子进程被终止并处于僵死状态时,父进程就会把子进程运行所使用的时间累加到自己进程中,释放子进程任务数据结构。
6.对该操作系统进程模型看法
正如Linux系统创始人在一篇新闻组投稿上所说的,要理解一个软件系统的真正运行机制,一定要阅读其源代码。
但由于目前Linux内核整个源代码的大小已经非常得大(例如2.2.20版具有268万行代码!!)所以本文基于Linux0.11操作系统的源代码,分析其进程模型。
虽然所选择的版本较低,各方面都有很大的提升空间,但该内核已能够正常编译运行,其中已经包括了Linux工作原理的精髓,与目前Linux内核基本功能较为相近,源代码又非常短小精干,因此会有极高的学习效率,能够做到事半功倍,快速入门。
7.参考资料
https://blog.csdn.net/cc289123557/article/details/53150536
http://ishare.iask.sina.com.cn/f/21489966.html
原文地址:https://www.cnblogs.com/yf9527/p/8977279.html