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

时间:2021-09-05 16:46:47

1. 前言

本文主要基于 Linux 0.12 的源代码,分析该 Linux 内核版本的进程模型及其调度器的算法。

Linux 0.12 源代码下载地址: http://oldlinux.org/Linux.old/kernel/0.1x/linux-0.12.tar.gz

2. 进程

2.1 什么是进程

  • 操作系统最核心的概念就是进程。进程简单来说就是在操作系统中运行的程序,它是操作系统资源管理的最小单位。
  • 进程是一个动态的实体,它是程序的一次执行过程。
  • 进程是一个具有一定独立功能的程序的一次运行活动,同时也是资源分配的最小单元。

2.2 进程的特征

  • 动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
  • 并发性:任何进程都可以同其他进程一起并发执行。
  • 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位。
  • 异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进。
  • 结构特征:进程由程序、数据和进程控制块三部分组成。

3. 进程的组织

3.1 进程控制块

在Linux内核中,通过一个被称为进程描述符的task_struct结构体来管理进程,这个结构体包含了一个进程所需的所有信息。它定义在此文件中(如下):

linux-2.6.38.8/include/linux/sched.h

task_struct 包含了这些内容:

①标示符 : 描述本进程的唯一标识符,用来区别其他进程。 

②状态 :任务状态,退出代码,退出信号等。 

③优先级 :相对于其他进程的优先级。 

④程序计数器:程序中即将被执行的下一条指令的地址。 

⑤内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。

⑥上下文数据:进程执行时处理器的寄存器中的数据。 

⑦ I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。 

⑧记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。 

3.1.1 进程的标识符(PID)

每个进程在系统中都有唯一的一个ID标识它,这个ID就是进程标识符(PID)。

在Linux 0.12中这样定义:

long pid;

3.1.2 进程的状态

① 进程的状态分析

long state; /* -1 unrunnable, 0 runnable, >0 stopped */

state成员的取值如下:

#define TASK_RUNNING 0

#define TASK_INTERRUPTIBLE 1

#define TASK_UNINTERRUPTIBLE 2

#define TASK_ZOMBIE 3

#define TASK_STOPPED 4

进程状态的分析:

  • TASK_RUNNING(运行状态)他是运行态和就绪态的合并,表示进程正在运行或准备运行。
  • TASK_INTERRUPTIBLE(可中断睡眠状态)进程正在睡眠(被阻塞),等待资源到来是唤醒,也可以通过其他进程信号或时钟中断唤醒,进入运行队列。
  • TASK_UNINTERRUPTIBLE(不可中断睡眠状态)其和浅度睡眠基本类似,但有一点就是不可其他进程信号或时钟中断唤醒。
  • TASK_STOPPED(暂停状态)进程暂停执行接受某种处理。
  • TASK_ZOMBIE(僵死状态)进程已经结束但未释放PCB,则称该进程处于僵死状态。

② 进程状态的转换

  • 运行状态 ===> 阻塞状态:例如正在运行的进程提出I/O请求,由运行状态转化为阻塞状态
  •  阻塞状态 ===> 就绪状态:例如I/O操作完成之后,由阻塞状态转化为就绪状态
  • 就绪状态 ===> 运行状态:例如就绪状态的进程被进程调度程序选中,分配到CPU中运行,由就绪状态转化为运行状态
  • 运行状态 ===> 就绪状态:处于运行状态的进程的时间片用完,不得不让出uCPU,由运行状态转化为就绪状态

③ 进程状态转换图

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

3.2 程序段

程序段就是能被进程调度程序调度到CPU执行的程序代码段。注意,程序可以被多个进程共享,就是说多个进程可以运行同一个程序。

3.3 数据段

一个进程的数据段,可以是进程对应的程序加工处理的原始数据,也可以是程序执行时产生的中间或最终结果。

4. 进程的调度

4.1 什么是进程调度 

       现代的操作系统都是多任务的操作系统,尽管随着科技的发展,硬件的处理器核心越来越多,但是仍然不能保证一个进程对应一个核心,这就势必需要一个管理单元,负责调度进程,由管理单元来决定下一刻应该由谁使用CPU,这里充当管理单元的就是进程调度器。

进程调度器的任务:1、分配时间给进程     2、上下文切换

4.2 调度器的演变

 

字段 版本
O(n)的始调度算法 linux-0.11~2.4
O(1)调度器 linux-2.5
CFS调度器 linux-2.6~至今

4.3 了解主调度器函数

  • 内核调度程序的初始化子程序 
void sched_init(void)  
{  
    int i;  
    struct desc_struct * p;     // 描述符表结构指针  
  
// Linux系统开发之初,内核不成熟.内核代码会被经常修改.Linus怕无意中修改了这些关键性的数据结构,造成与POSIX标准的不兼容.这里加入下面这个判断  
// 语句并无必要,纯粹是为了提醒自己以及其他修改内核代码的人.          
    if (sizeof(struct sigaction) != 16)     // sigaction是存放有关信号状态的结构.  
        panic("Struct sigaction MUST be 16 bytes");  
// 在全局描述符表中设置初始任务(任务0)的任务状态段描述符和局部数据表描述符.  
// FIRST_TSS_ENTRY和FIRST_LDT_ENTRY的值分别是4和5,定义在include/linux/sched.h中.gdt是一个描述符表数组(include/linux/head.h),  
// 实际上对应程序head.s中的他已描述符表基址(gdt).因此gdt+FIRST_TSS_ENTRY即为gdt[FIRST_TSS_ENTRY](即是gdt[4]),即gdt数组第4项的地址  
// 参见include/asm/system.h          
    set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));  
    set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));  
// 清任务数组和描述符表项(注意i=1开始,所以初始任务的描述符还在).描述符项结构定义在文件include/linux/head.h中.          
    p = gdt+2+FIRST_TSS_ENTRY;  
    for(i=1;i<NR_TASKS;i++) {  
        task[i] = NULL;  
        p->a=p->b=0;  
        p++;  
        p->a=p->b=0;  
        p++;  
    }          
/* Clear NT, so that we won't have troubles with that later on */  
/* 清除标志寄存器中的位NT,这样以后就不会有麻烦 */  
// EFLAGS中的NT标志位用于控制任务的嵌套调用.当NT位置位时,那么当前中断任务执行IRET指令时就会引起任务切换.NT指出TSS中的back_link字段是否有效.  
// NT=0时无效.          
    __asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");  
// 将任务0的TSS段选择符加载到任务寄存器tr.将局部描述符表段选择符加载到局部描述符表寄存器ldtr中.注意!!是将GDT中相应LDT描述符的选择符加载到ldtr.  
// 只明确加这一次,以后新任务LDT的加载,是CPU根据TSS中的LDT项自动加载.  
    ltr(0);     // 定义在include/linux/sched.h  
    lldt(0);    // 其中参数(0)是任务号.     
// 下面代码用于初始化8253定时器.通道0,选择工作方式3,二进制计数方式.通道0的输出引脚接在中断控制主芯片的IRQ0上,它每10毫秒发出一个IRQ0请求.  
// LATCH是初始定时计数值.          
    outb_p(0x36,0x43);      /* binary, mode 3, LSB/MSB, ch 0 */  
    outb_p(LATCH & 0xff , 0x40);    /* LSB */   // 定时值低字节  
    outb(LATCH >> 8 , 0x40);  /* MSB */   // 定时值高字节  
// 设置时钟中断处理程序句柄(设置时钟中断门).修改中断控制器屏蔽码,允许时钟中断.  
// 然后设置系统调用中断门.这两个设置中断描述衔表IDT中描述符的宏定义在文件include/asm/system.h中.两者的区别参见system.h文件开始处的说明.          
    set_intr_gate(0x20,&timer_interrupt);  
    outb(inb_p(0x21)&~0x01,0x21);  
    set_system_gate(0x80,&system_call);          
}  
  • schedule()就是主调度器的函数, 在内核中的许多地方, 如果要将CPU分配给与当前活动进程不同的另一个进程, 都会直接调用主调度器函数schedule()
void schedule(void)  
{  
    int i,next,c;  
    struct task_struct ** p;    // 任务结构指针的指针.  
  
/* check alarm, wake up any interruptible tasks that have got a signal */  
/* 检测alarm(进程的报警定时值),唤醒任何已得到信号的可中断任务 */          
  
// 从任务数组中最后一个任务开始循环检测alarm.在循环时跳过空指针项.  
    for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)  
        if (*p) {  
// 如果设置过任务超时定时timeout,并且已经超时,则复位超时定时值,并且如果任务处于可中断睡眠状态TASK_INTERRUPTIBLE下,将其置为就绪  
// 状态(TASK_RUNNING).  
            if ((*p)->timeout && (*p)->timeout < jiffies) {  
                (*p)->timeout = 0;  
                if ((*p)->state == TASK_INTERRUPTIBLE)  
                    (*p)->state = TASK_RUNNING;  
            }  
// 如果设置过任务的定时值alarm,并且已经过期(alarm<jiffies),则在信号位图中置SIGALRM信号,即向任务发送SIGALARM信号.然后清alarm.  
// 该信号的默认操作是终止进程.jiffies是系统从开机开始算起的滴答数(10ms/滴答).定义在sched.h中.                          
            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;  
        }  
  
/* this is the scheduler proper: */  
/* 这里是调度程序的主要部分 */  
  
    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的结果,或者後方中没有一个可运行的任务存在(此时c仍然为-1,next=0),则退出开始的循环,执行161行上的任务切换  
// 操作.否则就根据每个任务的优先权值,更新每一个任务的counter值,然后回到125行重新比较.counter值的计算方式为counter = counter /2 +priority.  
// 注意,这里计算过程不考虑进程的状态.                  
        if (c) break;  
        for(p = &LAST_TASK ; p > &FIRST_TASK ; --p)  
            if (*p)  
                (*p)->counter = ((*p)->counter >> 1) +  
                        (*p)->priority;  
    }  
// 用下面的宏(定义在sched.h中)把当前任务指针current指向任务号为next的任务,并切换到该任务中运行.在146行上next被初始化为0.因此若系统中没有任何  
// 其他任务可运行时,则next始终为0.因此调度函数会在系统空闲时去执行任务0.此时任务0权执行pause()          
    switch_to(next);        // 切换到任务号为next的任务,并运行之.          
}  

4.4 O(n)调度算法

  • 需要遍历可运行队列
  • O(n)存在的问题:调度器选择进程时需要遍历整个Runqueue,因此该算法的执行时间与进程数成正比。另外每次重新计算Counter所花费的时间也会随着系统中进程数的增加而线性增长。

5. 对操作系统进程模型的看法

         进程是指在系统中正在运行的一个应用程序,程序一旦运行就是进程(进程是指程序执行时的一个实例)。从内核的观点看,进程的目的就是担当分配系统资源(CPU时间、内存等)的基本单位。在计算机中,CPU是最宝贵的资源,为了提高CPU的利用率,所以进程的调度是很重要的。从Linux进程调度的演变来看,从O(n)到O(1),再到 都取得了很大的成功,但随着科技的快速发展,Linux的进程调度也更要精益求精。

6. 参考资料

https://blog.csdn.net/npy_lp/article/details/7292563

http://blog.chinaunix.net/uid-26126915-id-2948970.html

https://blog.csdn.net/wsrspirit/article/details/45850565