一、调研进程的调度算法
进程调度:首先,进程调度在操作系统来讲是一种资源分配。因为系统的资源有限,但是可能有多个进程都在同时进行都需要资源,这时操作系统就要按照一定的规则来进行进程调度以此来合理分配资源。
1、调度程序
(1)调度程序负责各个进程为分配处理器。Linux中无法停止当前正在运行的进程,只能被动地由调度程序将其选择为运行进程,或是切换为等待状态;
(2)Linux的进程调度由schedule()函数负责,其任务是在run_queue(运行队列)中选择一个就绪进程。任何进程,当它从系统调用返回时,都会转入schedule(),而中断处理函数完成它们的响应任务以后,也会进入schedule();
(3)每一个进程都有一个调度策略,在它的task_struct中规定policy(后文会详解),或为SCHED_RR,SCHED_FIFO,或为SCHED_OTHER。前两种为实时进程调度策略,后一种为普通进程调度策略。
2、进程切换
(1)进程的运行模式分为用户模式、内核模式(系统模式)。由调度程序通过系统调用来切换两种模式的进程;
(2)切换进程的选择,依据一定的优先级来选择可运行进程(也就是等待系统分配资源的进程)
可运行进程:在等待CPU资源的进程;
不可运行进程:在等待其他资源的进程;
(3)抢先式的调度方法,每个进程最多运行200ms(时间片);
3、基本概念
(1)进程一般分为两种:一般进程(优先级低)、实时进程(优先级高);
(2)priority(优先级):系统为进程设定的优先级,实际上是允许进程运行的时间值,从进程开始运行时算起;
(3)counter(计数器):进程运行的时间值,开始运行时设置为priority(200ms),每次时间中断该值减1;
(4)rt_priority(实时优先级):系统为进程设定的相对优先级;
(5)实时进程的调度策略:循环赛、先进先出;
4、几种进程调度方法
(1)所有任务都采用Linux分时调度策略
1)创建任务采用分时调度策略,并指定优先级nice值(-20~19);
2)将根据每个任务的nice值确定在CPU上的执行时间(counter);
3)若无等待资源,则将该任务加到就绪队列中;
4)调度程序遍历就绪队列中的任务,通过对每个任务动态优先级的计算(counter+20-nice)结果,选择计算结果最大的一个进程去运行。当这个时间片用完后(counter减至0)或主动放弃CPU时,该任务将被放在就绪队列队尾(时间片用完)或等待队列(因等待资源放弃CPU)中;
5)此时,调度程序重复上面计算过程,转到第4步;
6)当调度程序发现就绪任务计算所得的权值都不大于0时,重复第二步。
(2)所有任务采用FIFO调度策略(实时调度)
1)创建进程时指定采用FIFO,并设置实时优先级rt_priority(1-99);
2)若无等待资源,则将该任务加到就绪队列中;
3)调度程序遍历就绪队列,根据实时优先级计算调度权值(100+rt_priority),选择权值最高的任务使用CPU。该FIFO任务将一直占有CPU直到有优先级更高的任务就绪(即使优先级相同也不行)或主动放弃(等待资源);
4)调度程序发现有优先级更高的程序到达(高优先级任务可能被中断或是定时器任务唤醒,再或被当前运行的任务唤醒等),则调度程序立即在当前任务堆栈中保存当前CPU寄存器的所有数据,重新从高优先级任务的堆栈中加载寄存器数据到CPU,此时高优先级任务开始运行,重复第3步;
5)若当前任务因等待资源而主动放弃CPU所有权,则该任务将从就绪队列中删除,加入等待队列,此时重复第3步。
(3)所有任务采用RR调度策略(实时调度)
1)创建任务时指定调度参数为RR,并设置任务的实时优先级和nice值(nice值将会转换为该任务的时间片的长度);
2)若无等待资源,则将该任务加到就绪队列中;
3)调度程序遍历就绪队列,根据实时优先级计算调度权值(100+rt_priority),选择权值最高的任务使用CPU;
4)若就绪队列中的RR任务时间片为0,则会根据nice值设置该任务的时间片,同时将该任务放入就绪队列的队尾,重复步骤3;
5)当前任务由于等待资源而主动提出CPU,则加入等待队列中,重复步骤3。
5、系统中既有分时调度,又有时间片轮转调度和先进先出调度
(1)RR调度和FIFO调度的进程属于实时进程,以分时调度的进程是非实时进程;
(2)当实时进程准备就绪时,若当前CPU正在运行非实时进程,则实时进程立即抢占非实时进程;
(3)RR进程和FIFO进程都采用实时优先级作为调度的权值标准,RR是FIFO的一个延伸。FIFO时,若两个进程的优先级一样,则这两个进程的优先级一样的进程具体执行哪一个是由其在队列中的位置决定的,这样导致一些不公平性(优先级一样,为何让你先运行?)。若将两优先级一样的任务的调度策略都设定为RR,则保证了这两个任务可以循环执行,保证了公平。
二、调研tack_struct结构体,理解结构体中各个字段的含义
task_struct是Linux中描述进程的结构体,即Linux下的PCB,它是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含进程的信息。
进程的信息可以通过/proc系统文件夹查看。如要获取PID为1的进程信息,需要查看/proc/1这个文件夹
1、 task_struct主要内容
标识符:描述本进程的唯一标识符,用以区别其他进程
状态:任务状态,退出代码,退出信号等
优先级:相对于其他进程的优先级
程序计数器:程序中即将被执行的下一条指令的地址
内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
上下文数据:进程执行时处理器的寄存器中的数据
I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表
记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等
还有其他信息
具体信息见下图:
struct task_struct {
volatile long state; //-1不能运行 0运行 >0停止
unsigned long flags; //进程标志,在下面定义
int sigpending; //进程上是否有待处理的信号
mm_segment_t addr_limit;
volatile long need_resched; //调度标志,表示该进程是否需要重新调度,若非0,则当从内核态返回到用户态,会发生调度
int lock_depth; //锁深度
long counter; //进程可运行的时间量
long nice; //进程的时间片
unsigned long policy; //进程的调度策略,有三种,实时进程:SCHED_FIFO,SCHED_RR;分时进程:SCHED_OTHER;
struct mm_struct *mm; //进程内存管理信息
int processor;
unsigned long cpus_runnable, cpus_allowed;
struct list_head run_list; //指向运行队列的指针
unsigned long sleep_time; //进程的睡眠时间
struct task_struct *next_task, *prev_task; //用于将系统中所有的进程连成一个双向循环链表,其根是init_task.
struct mm_struct *active_mm;
struct list_head local_pages; //指向本地页面
unsigned int allocation_order, nr_local_pages;
struct linux_binfmt *binfmt; //进程所运行的可执行文件的格式
int exit_code, exit_signal;
int pdeath_signal; //父进程终止是向子进程发送的信号
unsigned long personality; //Linux可以运行由其他UNIX操作系统生成的符合iBCS2标准的程序
int did_exec:1; //按POSIX要求设计的布尔量,区分进程正在执行从父进程中继承的代码,还是执行由execve装入的新程序代码
pid_t pid; //进程标识符,用来代表一个进程
pid_t pgrp; //进程组标识,表示进程所属的进程组
pid_t tty_old_pgrp; //进程控制终端所在的组标识
pid_t session; //进程的会话标识
pid_t tgid;
int leader; //标志,表示进程是否为会话主管
//指针指向(原始的)父进程,孩子进程,比自己年轻的兄弟进程,比自己年长的兄弟进程。(p->father能被p->p_pptr->pid代替)
struct task_struct *p_opptr, *p_pptr, *p_cptr, *p_ysptr, *p_osptr;
struct list_head thread_group; //线程链表
//进程散列表指针
struct task_struct *pidhash_next; //用于将进程链入HASH表pidhash
struct task_struct **pidhash_pprev;
wait_queue_head_t wait_chldexit; //wait4()使用
struct completion *vfork_done; // vfork() 使用
unsigned long rt_priority; //实时优先级,计算实时进程调度的weight值
it_real_value,it_real_incr //用于REAL定时器,单位为jiffies。系统根据it_real_value //设置定时器的第一个终止时间。在定时器到期时,向进程发送SIGALRM信号,同时根据it_real_incr重置终止时间
it_prof_value,it_prof_incr //用于Profile定时器,单位为jiffies。当进程运行时,不管在何种状态下,每个tick都使it_prof_value值减一,当减到0时,向进程发送信号SIGPROF,并根据it_prof_incr重置时间
it_virt_value,it_virt_value //用于Virtual定时器,单位为jiffies。当进程运行时,不管在何种状态下,每个tick都使it_virt_value值减一,当减到0时,向进程发送信号SIGVTALRM,根据it_virt_incr重置初值。
//Real定时器根据系统时间实时更新,不管进程是否在运行Virtual定时器只在进程运行时,根据进程在用户态消耗的时间更新Profile定时器在进程运行时,根据进程消耗的时(不管在用户态还是内核态)更新
//unsigned long it_real_value, it_prof_value, it_virt_value;
//unsigned long it_real_incr, it_prof_incr, it_virt_value;
struct timer_list real_timer; //指向实时定时器的指针
struct tms times; //记录进程消耗的时间,
unsigned long start_time; //进程创建的时间
long per_cpu_utime[NR_CPUS], per_cpu_stime[NR_CPUS]; //记录进程在每个CPU上所消耗的用户态时间和核心态时间
//内存缺页和交换信息:
min_flt, maj_flt //累计进程的次缺页数(Copy on Write页和匿名页)和主缺页数(从映射文件或交换设备读入的页面数);
nswap //记录进程累计换出的页面数,即写到交换设备上的页面数。
cmin_flt, cmaj_flt, cnswap //记录本进程为祖先的所有子孙进程的累计次缺页数,主缺页数和换出页面数。在父进程回收终止的子进程时,父进程会将子进程的这些信息累计到自己结构的这些域中
unsigned long min_flt, maj_flt, nswap, cmin_flt, cmaj_flt, cnswap;
int swappable:1; //表示进程的虚拟地址空间是否允许换出
//进程认证信息
uid,gid //运行该进程的用户的用户标识符和组标识符,通常是进程创建者的uid,gid //euid,egid为有效uid,gid
fsuid,fsgid //文件系统uid,gid,这两个ID号通常与有效uid,gid相等,在检查对于文件系统的访问权限时使用他们。
suid,sgid //备份uid,gid
//uid_t uid,euid,suid,fsuid;
//gid_t gid,egid,sgid,fsgid;
int ngroups; //记录进程在多少个用户组中
gid_t groups[NGROUPS]; //记录进程所在的组
kernel_cap_t cap_effective, cap_inheritable, cap_permitted; //进程的权能,分别是有效位集合,继承位集合,允许位集合
int keep_capabilities:1;
struct user_struct *user;
struct rlimit rlim[RLIM_NLIMITS]; //与进程相关的资源限制信息
unsigned short used_math; //是否使用FPU
char comm[16]; //进程正在运行的可执行文件名
//文件系统信息
int link_count, total_link_count;
struct tty_struct *tty;
unsigned int locks;
//进程间通信信息
struct sem_undo *semundo; //进程在信号灯上的所有undo操作
struct sem_queue *semsleeping; //当进程因为信号灯操作而挂起时,他在该队列中记录等待的操作
//进程的CPU状态,切换时,要保存到停止进程的task_struct中
struct thread_struct thread;
struct fs_struct *fs;
//打开文件信息
struct files_struct *files;
//信号处理函数
spinlock_t sigmask_lock;
struct signal_struct *sig; //信号处理函数,
sigset_t blocked; //进程当前要阻塞的信号,每个信号对应一位
struct sigpending pending; //进程上是否有待处理的信号
unsigned long sas_ss_sp;
size_t sas_ss_size;
int (*notifier)(void *priv);
void *notifier_data;
sigset_t *notifier_mask;
u32 parent_exec_id;
u32 self_exec_id;
spinlock_t alloc_lock;
void *journal_info;
};
三、使用代码模拟实现僵尸进程和孤儿进程的场景
1、僵尸进程
(1)当子进程退出且父进程没有读取到子进程的退出信息时就会产生僵尸进程;
(2)僵尸进程会以终止状态保持在进程表中,并且会一直等待父进程读取退出状态码;
(3)所有,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程进入Z状态。
创建一个维持30s的僵尸进程的例子,如图
编译并在另一个终端下启动监控。
其中,ps aux 查看所有系统运行的程序;ps -l只能查看自己bash的程序
可以看到,子进程进入僵尸状态(Z状态)
2、孤儿进程
(1)父进程先退出,子进程就称为“孤儿进程”;
(2)孤儿进程会被1号进程即bash领养,也由它回收。
创建一个孤儿进程的例子,如图:
编译并在另一个终端下启动监控
可以看到,子进程成为孤儿进程