第一次作业:基于Linux0.11操作系统的进程模型分析

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

时间: 2024-10-04 04:49:31

第一次作业:基于Linux0.11操作系统的进程模型分析的相关文章

第一次作业:基于Linux2.6.30进程模型分析

前言 本篇基于Linux Kernel 2.6.30 的源代码,源码浏览地址如下:https://elixir.bootlin.com/linux/v2.6.30/source 具体内容分为: 操作系统是怎么组织进程的 进程状态如何转换(给出进程状态转换图) 进程是如何调度的 谈谈自己对该操作系统进程模型的看法 一.操作系统是怎么组织进程的 1.进程 1.1进程的引入 多道程序系统中,程序具有:并行.制约以及动态的特征.程序概念难以便是和反映系统中的情况: ①程序是一个静态的概念:程序是完成某个

第一次作业:基于Linux-0.12的进程分析

这次作业主要基于Linux-0.12的源代码,分析Linux是如何组织进程,进程的状态之间是如何转换,以及进程是如何调度的. 一. 进程的概念: 1.进程就是:程序在数据集合上的一次运行过程,是系统进行资源分配和调度的独立单位. 2.对进程的静态描述为:是一个数据集合,以及在其上运行的程序. 3.我本来认为进程与程序是差不多的东西,但发现他们其实并不一样,进程是一个动态的概念,不同于程序又依赖于程序,既有联系又有区别,进程具备程序不具备的特征,比如: 1).动态特征:进程具有生命周期,创建之后才

第一次作业:基于Orange&#39;s OS系统的进程模型分析与心得体会

1一. 操作系统进程概念模型与进程控制块概念浅析 1. 什么是进程? 图 1 - 1 (WIN10系统任务管理器对进程管理的图形化界面) 计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础. --百度百科 应用程序的实例.对正在运行的程序的抽象. --<现代操作系统> 2. 什么是进程控制块? 进程控制块(Processing Control Block),是操作系统核心中一种数据结构,主要表示进程状态.其作用是使一个在多道程序环境下不能独立

Linux-0.11内核源码分析系列:关于线性地址,逻辑地址,物理地址的关系与区别

/* *Author : DavidLin *Date : 2014-11-22pm *Email : [email protected] or [email protected] *world : the city of SZ, in China *Ver : 000.000.001 *history : editor time do * 1)LinPeng 2014-11-22 created this file! * 2) */     以下所有描述基于Linux0.11内核及其所编写的年

linux0.11内核fork实现分析(不看不知道,一看很简单)

pcDuino3下支持mmc启动,官方的Uboot是采用SPL框架实现的,因为内部的SRAM空间达到32K,我们完全可以在这32K空间内编写一个完整可用小巧的bootloader来完成引导Linux kernel的目的. 我们首先介绍下SPL框架,可以先看下<GNU ARM汇编--(十八)u-boot-采用nand_spl方式的启动方法>和<GNU ARM汇编--(十九)u-boot-nand-spl启动过程分析>,NAND_SPL也算是SPL框架下的一种模式. 当使用Nand f

第一次作业:基于Linux0.01深入源码分析进程模型

一.前言 本文主要基于Linux0.01源代码分析进程模型.Linux 0.01虽然是Linux的第一个发行版本,但是却基本具备了操作系统中最重要的组成部分,同时Linux 0.01只有8500行左右的代码,对于初学者而言学习起来比较简单一点. Linux 0.01源代码下载地址: https://mirrors.edge.kernel.org/pub/linux/kernel/Historic/ 二.进程的定义 进程是程序执行的基本单位.(其中,进程和程序的区别:程序指的是由若干函数组成的可执

第一次作业 基于Linux 0.12的进程模型分析

作业内容 挑选一个开源的操作系统,深入源码分析其进程模型,具体包含如下内容: 操作系统是怎么组织进程的 进程状态如何转换(给出进程状态转换图) 进程是如何调度的 谈谈自己对该操作系统进程模型的看法 1.操作系统是怎么组织进程的 1.1什么是进程 程序是一个可执行的文件,而进程是一个执行中的程序实例.Linux操作系统上利用分时技术,可同时运行多个进程.利用分时技术,在Linux操作系统上同时可以运行多个进程.分时技术的基本原理是把CPU的运行时间划分成一个个规定长度的时间片,让每个进程在一个时间

第一次作业:基于Linux进程模型分析

一.关于线程和进程 1.进程  进程是指在系统中正在运行的一个应用程序 2.线程 线程是系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元 3.进程与线程的关系  · 对于操作系统而言,其调度单元是线程.一个进程至少包括一个线程,通常将该线程称为主线程. · 一个进程从主线程的执行开始进而创建一个或多个附加线程,就是所谓基于多线程的多任务 · 一个线程可以创建和撤销另一个线程;同一个进程中的多个线程之间可以并发执行. · 相对进程而言,线程是一个更加接近于执行体的概念,它可以与同

第一次作业:基于Linux操作系统深入源码进程模型分析

1.Linux操作系统的简易介绍 Linux系统一般有4个主要部分:内核.shell.文件系统和应用程序.内核.shell和文件系统一起形成了基本的操作系统结构,它们使得用户可以运行程序.管理文件并使用系统. (1)内核 内核是操作系统的核心,具有很多最基本功能,如虚拟内存.多任务.共享库.需求加载.可执行程序和TCP/IP网络功能.Linux内核的模块分为以下几个部分:存储管理.CPU和进程管理.文件系统.设备管理和驱动.网络通信.系统的初始化和系统调用等. (2)shell shell是系统