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,由运行状态转化为就绪状态
③ 进程状态转换图
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