第1次作业:深入源码分析进程模型

一.进程的定义

    进程是60年代初首先由麻省理工学院的MULTICS系统和IBM公司的CTSS/360系统引入的

《现代操作系统》第四版定义:一个进程(sequential process)就是一个正在执行程序的实例,包括程序计数器,寄存器和变量的当前值。

 查阅相关资料得知,进程是一个具有一定独立功能的程序的一次运行活动,同时也是资源分配的最小单元,是操作系统结构的基础。

二.进程在操作系统上的组织

  首先就是要设计一个合理的数据结构用来描述进程的概念。这种结构就是进程控制块(Process Control Block,PCB)。系统为每个进程维护了一个PCB,用来保存与该进程有关的各种状态信息。PCB只是基本原理中的说法,对于一个真实的操作系统可能不叫PCB,比如Linux中叫做任务结构体(task struct),ask_struct存include/ linux/sched.h 中,其中包括管理进程所需的各种信息。

  物理寄存器只能有一份,但逻辑寄存器则是每个进程都有一份,逻辑寄存器是通过内存变量来实现的,那么这些存储的变量就对应着PCB的相应字段。PCB是进程的唯一标识。每生成一个新的进程,就要为它创造一个PCB然后对其进行初始化。若要撤销一个进程时只要回收PCB即可。进程切换就可以通过操作PCB进行。

   Linux系统的所有进程控制块组织成结构数组形式。早期的Linux版本是多可同时运行进程的个数由NR_TASK(缺省值为512)规定,NR_TASK即为PCB结果数组的长度。近期版本中的PCB组成一个环形结构,系统中实际存在的进程数由其定义的全局变量nr_task来动态记录。结构数组:struct task_struct *task[NR_TASK]={&init_task}来记录指向各PCB的指针,该指针数组定义于/kernel/sched.c中。

  在创建一个新进程时,系统在内存中申请一个空的task_struct区,即空闲PCB块,并填入所需信息,同时只想该结构的指针填入到task[]数组中。当前处于运行状态进程的PCB用指针数组curren_set[]来指出。这是因为Linux文件多处理机系统,系统内可能存在多个同时运行的进程,故curren_set[]定义成指针数组。

 示例如下:

struct task_struct {     
        volatile long state;    //进程状态  
        void *stack;            //内存指针  
        atomic_t usage;                  
        unsigned int flags;    //进程标号(进程名字)  
        unsigned int ptrace;                                        
  
        int lock_depth;        //BLK 锁深度  
  
#ifdef CONFIG_SMP  
#ifdef __ARCH_WANT_UNLOCKED_CTXSW            //配置多核多线程   
        int oncpu;  
#endif  
#endif  
  
        int prio, static_prio, normal_prio;  //进程的优先级  
        unsigned int rt_priority;            //实时进程的优先级  
        const struct sched_class *sched_class;   //调度器的指针  
        struct sched_entity se;              //调度器 实例化的对象  
        struct sched_rt_entity rt;          //实时 调度器的一个对象  
  
#ifdef CONFIG_PREEMPT_NOTIFIERS            //配置抢占通知器   
        /* struct preempt_notifier列表 */  
        struct hlist_head preempt_notifiers;     
#endif  
  
        /*fpu_count 里面内容是如果一个浮点运算器被使用,它记录着连续的上下文切换的次数,如果fpu_Count超过一个 
         临界值,不怎么工作的FPU会火力全开以至于当fpu_count超过 256次后才变得闲置下来,为了解决这个问题,FPU 
         仅仅使用一段时间 */                                 
        unsigned char fpu_counter;  //定义 fpu_count   
#ifdef CONFIG_BLK_DEV_IO_TRACE      //配置 BLK 锁开发版的输入输出跟踪器  
       unsigned int btrace_seq;           
#endif  
  
        unsigned int policy;        
        cpumask_t cpus_allowed;  
  
#ifdef CONFIG_TREE_PREEMPT_RCU     //配置抢占树,抢占的结构体的读写机制,即RCU机制。  
        int rcu_read_lock_nesting;  
        char rcu_read_unlock_special;  
        struct rcu_node *rcu_blocked_node;  
        struct list_head rcu_node_entry;  
#endif /* #ifdef CONFIG_TREE_PREEMPT_RCU */  
  
#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)  
        struct sched_info sched_info;          //调度器的状态   
#endif  
  
        struct list_head tasks;  
        struct plist_node pushable_tasks;  
  
        struct mm_struct *mm, *active_mm;     //虚拟地址空间的结构体   
  
                                 //进程退出时getpid 就获取status就是它。  
        int exit_state;                //task 状态 ,正常退出状态   
        int exit_code, exit_signal;   //退出信号  
        int pdeath_signal;    //当成为孤儿进程时发送信号   

        unsigned int personality;   //表明进程的状态     
        unsigned did_exec:1;  
        unsigned in_execve:1; //第一个表已经调过了exec族函数,已经发生了进程的程序替换 第二个代表该进程正在调用execve函数 第三个 正在等待i/o设备 第四个 表示当fork生成子进程时,是否恢复了进程的默认优先级  
        unsigned in_iowait:1;  

        /* 在分叉时恢复默认优先级/策略*/  
        unsigned sched_reset_on_fork:1;  
        pid_t pid;  
        pid_t tgid;  
  
#ifdef CONFIG_CC_STACKPROTECTOR        //配置堆栈保护措施  
        unsigned long stack_canary;     //canary值 保护编译器 防止堆栈溢出 导致的返回地址被填充   
#endif   
        struct task_struct *real_parent; 
        struct task_struct *parent;  
        struct list_head children;         //子节点和兄弟节点的定义  
        struct list_head sibling;       
        struct task_struct *group_leader;  //线程组的头结点  
       
        struct list_head ptraced;                  //跟踪器的头结点,跟踪器 跟踪 进程的逻辑流,即PC指令流  
        struct list_head ptrace_entry;              
   
        struct pid_link pids[PIDTYPE_MAX];   //定义 PID_LINK 结构体用它通过PID在哈希散列表中查找相应的task_struct  
        struct list_head thread_group;         //用来保存线程组的PID  
  
        struct completion *vfork_done;            
        int __user *set_child_tid;               //指向用户创造创立的线程的TID号   
        int __user *clear_child_tid;             //指向被清除的线程的TID号   
  
        cputime_t utime, stime, utimescaled, stimescaled;  
        cputime_t gtime;  
        cputime_t prev_utime, prev_stime;  
        unsigned long nvcsw, nivcsw;   //上下文切换的次数   
        struct timespec start_time;             
        struct timespec real_start_time;        
        unsigned long min_flt, maj_flt;  
  
        struct task_cputime cputime_expires;  
        struct list_head cpu_timers[3];  
  
        const struct cred *real_cred;   
        const struct cred *cred;         
        struct mutex cred_guard_mutex;  
        struct cred *replacement_session_keyring; 
  
        char comm[TASK_COMM_LEN]; 
                         //文件系统信息  
        int link_count, total_link_count;  
#ifdef CONFIG_SYSVIPC        //配置进程的通信机制  
 
        struct sysv_sem sysvsem;  
#endif  
#ifdef CONFIG_DETECT_HUNG_TASK   
        unsigned long last_switch_count;  
#endif       
        struct thread_struct thread;       //CPU特殊状态的测试,线程结构体   
        struct fs_struct *fs;    //fs 指向一个文件系统信息结构体,该结构体有文件系统的信息  
     //指向记录打开文件信息的 结构体   
        struct files_struct *files;  
        //命名空间的定义   
        struct nsproxy *nsproxy;  
        //配置进程的信号处理    
        struct signal_struct *signal;   //以下是普通信号部分  
        struct sighand_struct *sighand; //这个指向 handler表   
  
        sigset_t blocked, real_blocked;  //这个表示进程的屏蔽字  
        sigset_t saved_sigmask;  
        struct sigpending pending; //pending表  
  
        unsigned long sas_ss_sp;  // 以下是实时信号部分  
        size_t sas_ss_size;  
        int (*notifier)(void *priv);  
        void *notifier_data;  
        sigset_t *notifier_mask;  
        struct audit_context *audit_context;  
#ifdef CONFIG_AUDITSYSCALL   // 配置系统调用   
        uid_t loginuid;  
        unsigned int sessionid;  
#endif  
        seccomp_t seccomp;  
  
#ifdef CONFIG_UTRACE  
        struct utrace *utrace;  
        unsigned long utrace_flags;  
#endif  
        u32 parent_exec_id;  
        u32 self_exec_id;  
/* 配置器保护措施配置  */
        spinlock_t alloc_lock;  
  
#ifdef CONFIG_GENERIC_HARDIRQS  
        struct irqaction *irqaction;  
#endif  
        spinlock_t pi_lock;  
  
#ifdef CONFIG_RT_MUTEXES  // 互斥的配置    
        struct plist_head pi_waiters;  
        struct rt_mutex_waiter *pi_blocked_on;  
#endif  
#ifdef CONFIG_LOCKDEP                 // 死锁模块的配置   
# define MAX_LOCK_DEPTH 48UL  
        u64 curr_chain_key;  
        int lockdep_depth;  
        unsigned int lockdep_recursion;  
        struct held_lock held_locks[MAX_LOCK_DEPTH];  
        gfp_t lockdep_reclaim_gfp;  
#endif  
  
   // 文件系统的日志信息  
        void *journal_info;          
        struct bio *bio_list, **bio_tail;  
   
//VM 虚拟机的状态   
        struct reclaim_state *reclaim_state;  
  
        struct backing_dev_info *backing_dev_info;  
  
        struct io_context *io_context;  
  
        unsigned long ptrace_message;  
        siginfo_t *last_siginfo; 
        struct task_io_accounting ioac;  
#ifdef CONFIG_CPUSETS  
       nodemask_t mems_allowed;      //定义一个结构体 标志 内存是否允许访问 保护配置器的锁的  
#ifndef __GENKSYMS__   
        unsigned short cpuset_mem_spread_rotor;  
        unsigned short cpuset_slab_spread_rotor;  
        int mems_allowed_change_disable;  
#else  
        int cpuset_mem_spread_rotor;  
        int cpuset_slab_spread_rotor;  
#endif  
#endif  
#ifdef CONFIG_CGROUPS // 配置控制组信息   
        struct css_set *cgroups;  
        struct list_head cg_list;  
#endif 
#endif  
};  

三.进程状态的转换

  1.进程的三态模型 
  在多道程序系统中,进程在处理器上交替运行,状态也不断地发生变化。进程一般有3种基本状态:运行、就绪和阻塞。 
  (1)运行:当一个进程在处理机上运行时,则称该进程处于运行状态。处于此状态的进程的数目小于等于处理器的数目,对于单处理机系统,处于运行状态的进程只有一个。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。 
  (2)就绪:当一个进程获得了除处理机以外的一切所需资源,一旦得到处理机即可运行,则称此进程处于就绪状态。就绪进程可以按多个优先级来划分队列。例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。 
  (3)阻塞:也称为等待或睡眠状态,一个进程正在等待某一事件发生(例如请求I/O而等待I/O完成等)而暂时停止运行,这时即使把处理机分配给进程也无法运行,故称该进程处于阻塞状态。 
这里写图片描述 
2.进程的五态模型 
  五态模型:对于一个实际的系统,进程的状态及其转换更为复杂。引入新建态和终止态构成了进程的五态模型。 
  新建态: 对应于进程刚刚被创建时没有被提交的状态,并等待系统完成创建进程的所有必要信息。 进程正在创建过程中,还不能运行。操作系统在创建状态要进行的工作包括分配和建立进程控制块表项、建立资源表格(如打开文件表)并分配资源、加载程序并建立地址空间表等。创建进程时分为两个阶段,第一个阶段为一个新进程创建必要的管理信息,第二个阶段让该进程进入就绪状态。由于有了新建态,操作系统往往可以根据系统的性能和主存容量的限制推迟新建态进程的提交。 
  终止态:进程已结束运行,回收除进程控制块之外的其他资源,并让其他进程从进程控制块中收集有关信息(如记帐和将退出代码传递给父进程)。类似的,进程的终止也可分为两个阶段,第一个阶段等待操作系统进行善后处理,第二个阶段释放主存。 
这里写图片描述 
3.细分进程状态及其转换 
  由于进程的不断创建,系统资源特别是主存资源已不能满足所有进程运行的要求。这时,就必须将某些进程挂起,放到磁盘对换区,暂时不参加调度,以平衡系统负载;进程挂起的原因可能是系统故障,或者是用户调试程序,也可能是需要检查问题。 
  活跃就绪:是指进程在主存并且可被调度的状态。 
  静止就绪(挂起就绪):是指进程被对换到辅存时的就绪状态,是不能被直接调度的状态,只有当主存中没有活跃就绪态进程,或者是挂起就绪态进程具有更高的优先级,系统将把挂起就绪态进程调回主存并转换为活跃就绪。 
  活跃阻塞:是指进程已在主存,一旦等待的事件产生便进入活跃就绪状态。 
  静止阻塞:是指进程对换到辅存时的阻塞状态,一旦等待的事件产生便进入静止就绪状态。 
这里写图片描述

四.进程的调度

发展历史

Linux从2.5版本开始引入调度器,后在2.6版本中将公平的的调度概念引入了调度程序,代替之前的调度器,成为CFS完全公平调度算法)。

策略

I/O消耗型和处理器消耗型

I/O消耗型进程是指那些大部分时间都在等待I/O操作的进程,处理器耗费型的进程则是指把大多数时间用于执行代码的进程,除非被抢占,他们一般都一直在运行。

为了保证交互式应用和桌面系统的性能,一般Linux更倾向于优先调度I/O消耗型进程。

进程优先级

Linux采用了两种不同的优先级范围。

  1. 使用nice值:越大的nice值意味着更低的优先级。 (-19 ~ 20之间)
  2. 实时优先级:可配置,越高意味着进程优先级越高。
任何实时的进程优先级都高于普通的进程,因此上面的两种优先级范围处于互不相交的范畴。
  1. 时间片:Linux中并不是以固定的时间值(如10ms)来分配时间片的,而是将处理器的使用比作为“时间片”划分给进程。这样,进程所获得的实际CPU时间就和系统的负载密切相关。
Linux中的抢占时机取决于新的可运行进程消耗了多少处理器使用比,
如果消耗的使用比当前进程小,则立刻投入运行,否则将推迟其运行。

Linux调度的实现

下面我们来看看CFS是如何实现的,一般我们把它分为4个主要的部分来分析。

时间记账

所有的调度器都必须对进程的运行时间记账,换句话说就是要知道当前调度周期内,进程还剩下多少个时间片可用(这将会是抢占的一个重要标准)

1. 调度器实体结构

CFS中用于记录进程运行时间的数据结构为“调度实体”,这个结构体被定义在中:

struct sched_entity {
	/* 用于进行调度均衡的相关变量,主要跟红黑树有关 */
	struct load_weight		load; // 权重,跟优先级有关
	unsigned long			runnable_weight; // 在所有可运行进程中所占的权重
	struct rb_node			run_node; // 红黑树的节点
	struct list_head		group_node; // 所在进程组
	unsigned int			on_rq; // 标记是否处于红黑树运行队列中

	u64				exec_start; // 进程开始执行的时间
	u64				sum_exec_runtime; // 进程总运行时间
	u64				vruntime; // 虚拟运行时间,下面会给出详细解释
	u64				prev_sum_exec_runtime; // 进程在切换CPU时的sum_exec_runtime,简单说就是上个调度周期中运行的总时间

	u64				nr_migrations;

	struct sched_statistics		statistics;
	
	// 以下省略了一些在特定宏条件下才会启用的变量
}

注:本文中所有用到的linux源码均来自linux在github上官方的git库(2018.01)

2. 虚拟实时 (vruntime)

现在我们来谈谈上面结构体中的vruntime变量所表示的意义。我们称它为“虚拟运行时间”,该运行时间的计算是经过了所有可运行进程总数的标准化(简单说就是加权的)。它以ns为单位,与定时器节拍不再相关。

可以认为这是CFS为了能够实现理想多任务处理而不得不虚拟的一个新的时钟,具体地讲,一个进程的vruntime会随着运行时间的增加而增加,但这个增加的速度由它所占的权重来决定。

结果就是权重越高,增长越慢:所得到的调度时间也就越小 —— CFS用它来记录一个程序到底运行了多长时间以及还应该运行多久。

下面我们来看一下这个记账功能的实现源码()

/*
 * Update the current task's runtime statistics.
 */
static void update_curr(struct cfs_rq *cfs_rq)
{
	struct sched_entity *curr = cfs_rq->curr;
	u64 now = rq_clock_task(rq_of(cfs_rq));
	u64 delta_exec;

	if (unlikely(!curr))
		return;
	
	// 获得从最后一次修改负载后当前任务所占用的运行总时间
	delta_exec = now - curr->exec_start;
	if (unlikely((s64)delta_exec <= 0))
		return;
		
	// 更新执行开始时间
	curr->exec_start = now;

	schedstat_set(curr->statistics.exec_max,
		      max(delta_exec, curr->statistics.exec_max));

	curr->sum_exec_runtime += delta_exec;
	schedstat_add(cfs_rq->exec_clock, delta_exec);

	// 计算虚拟时间,具体的转换算法写在clac_delta_fair函数中
	curr->vruntime += calc_delta_fair(delta_exec, curr);
	update_min_vruntime(cfs_rq);

	if (entity_is_task(curr)) {
		struct task_struct *curtask = task_of(curr);

		trace_sched_stat_runtime(curtask, delta_exec, curr->vruntime);
		cgroup_account_cputime(curtask, delta_exec);
		account_group_exec_runtime(curtask, delta_exec);
	}

	account_cfs_rq_runtime(cfs_rq, delta_exec);
}

该函数计算了当前进程的执行时间,将其存放在变量中,然后使用函数计算对应的虚拟运行时间,并更新值。

这个函数是由系统定时器周期性调用的(无论进程的状态是什么),因此vruntime可以准确地测量给定进程的运行时间,并以此为依据推断出下一个要运行的进程是什么。

进程选择

这里便是调度的核心部分,用一句话来梗概CFS算法的核心就是选择具有最小vruntime的进程作为下一个需要调度的进程。

为了实现选择,当然要维护一个可运行的进程队列(教科书上常说的ready队列),CFS使用了红黑树来组织这个队列。

红黑树是一种非常著名的数据结构,但这里我们不讨论它的实现和诸多特性(过于复杂),我们记住:红黑树是一种自平衡二叉树,再简单一点,它是一种以树节点方式储存数据的结构,每个节点对应了一个键值,利用这个键值可以快速索引树上的数据,并且它可以按照一定的规则自动调整每个节点的位置,使得通过键值检索到对应节点的速度和整个树节点的规模呈指数比关系。

1. 找到下一个任务节点

先假设一个红黑树储存了系统中所有的可运行进程,节点的键值就是它们的vruntime,CFS现在要找到下一个需要调度的进程,那么就是要找到这棵红黑树上键值最小的那个节点:就是最左叶子节点。

实现此过程的源码如下():

static struct sched_entity *
pick_next_entity(struct cfs_rq *cfs_rq, struct sched_entity *curr)
{
	struct sched_entity *left = __pick_first_entity(cfs_rq);
	struct sched_entity *se;

	/*
	 * If curr is set we have to see if its left of the leftmost entity
	 * still in the tree, provided there was anything in the tree at all.
	 */
	if (!left || (curr && entity_before(curr, left)))
		left = curr;

	se = left; /* ideally we run the leftmost entity */

	/*
	 * 下面的过程主要针对一些特殊情况,我们在此不做讨论
	 */
	if (cfs_rq->skip == se) {
		struct sched_entity *second;

		if (se == curr) {
			second = __pick_first_entity(cfs_rq);
		} else {
			second = __pick_next_entity(se);
			if (!second || (curr && entity_before(curr, second)))
				second = curr;
		}

		if (second && wakeup_preempt_entity(second, left) < 1)
			se = second;
	}

	if (cfs_rq->last && wakeup_preempt_entity(cfs_rq->last, left) < 1)
		se = cfs_rq->last;

	if (cfs_rq->next && wakeup_preempt_entity(cfs_rq->next, left) < 1)
		se = cfs_rq->next;

	clear_buddies(cfs_rq, se);

	return se;
}

2. 向队列中加入新的进程

向可运行队列中插入一个新的节点,意味着有一个新的进程状态转换为可运行,这会发生在两种情况下:一是当进程由阻塞态被唤醒,二是fork产生新的进程时。

将其加入队列的过程本质上来说就是红黑树插入新节点的过程:

static void
enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
	bool renorm = !(flags & ENQUEUE_WAKEUP) || (flags & ENQUEUE_MIGRATED);
	bool curr = cfs_rq->curr == se;

	/*
	 * 如果要加入的进程就是当前正在运行的进程,重新规范化vruntime
	 * 然后更新当前任务的运行时统计数据
	 */
	if (renorm && curr)
		se->vruntime += cfs_rq->min_vruntime;

	update_curr(cfs_rq);

	/*
	 * Otherwise, renormalise after, such that we're placed at the current
	 * moment in time, instead of some random moment in the past. Being
	 * placed in the past could significantly boost this task to the
	 * fairness detriment of existing tasks.
	 */
	if (renorm && !curr)
		se->vruntime += cfs_rq->min_vruntime;

	/*
	 * 更新对应调度器实体的各种记录值
	 */
	 
	update_load_avg(cfs_rq, se, UPDATE_TG | DO_ATTACH);
	update_cfs_group(se);
	enqueue_runnable_load_avg(cfs_rq, se);
	account_entity_enqueue(cfs_rq, se);

	if (flags & ENQUEUE_WAKEUP)
		place_entity(cfs_rq, se, 0);

	check_schedstat_required();
	update_stats_enqueue(cfs_rq, se, flags);
	check_spread(cfs_rq, se);
	if (!curr)
		__enqueue_entity(cfs_rq, se); // 真正的插入过程
	se->on_rq = 1;

	if (cfs_rq->nr_running == 1) {
		list_add_leaf_cfs_rq(cfs_rq);
		check_enqueue_throttle(cfs_rq);
	}
}

上面的函数主要用来更新运行时间和各类统计数据,然后调用来把数据真正插入红黑树中:

static void __enqueue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	struct rb_node **link = &cfs_rq->tasks_timeline.rb_root.rb_node;
	struct rb_node *parent = NULL;
	struct sched_entity *entry;
	bool leftmost = true;

	/*
	 * 在红黑树中搜索合适的位置
	 */
	while (*link) {
		parent = *link;
		entry = rb_entry(parent, struct sched_entity, run_node);
		/*
		 * 具有相同键值的节点会被放在一起
		 */
		if (entity_before(se, entry)) {
			link = &parent->rb_left;
		} else {
			link = &parent->rb_right;
			leftmost = false;
		}
	}

	rb_link_node(&se->run_node, parent, link);
	rb_insert_color_cached(&se->run_node,
			       &cfs_rq->tasks_timeline, leftmost);
}

while()循环是遍历树以寻找匹配键值的过程,也就是搜索一颗平衡树的过程。找到后我们对要插入位置的父节点执行来将节点插入其中,然后更新红黑树的自平衡相关属性。

3. 从队列中移除进程

从队列中删除一个节点有两种可能:一是进程执行完毕退出,而是进程受到了阻塞。

static void
dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se, int flags)
{
	/*
	 * 更新“当前进程”的运行统计数据
	 */
	update_curr(cfs_rq);

	/*
	 * When dequeuing a sched_entity, we must:
	 *   - Update loads to have both entity and cfs_rq synced with now.
	 *   - Substract its load from the cfs_rq->runnable_avg.
	 *   - Substract its previous weight from cfs_rq->load.weight.
	 *   - For group entity, update its weight to reflect the new share
	 *     of its group cfs_rq.
	 */
	update_load_avg(cfs_rq, se, UPDATE_TG);
	dequeue_runnable_load_avg(cfs_rq, se);

	update_stats_dequeue(cfs_rq, se, flags);

	clear_buddies(cfs_rq, se);

	if (se != cfs_rq->curr)
		__dequeue_entity(cfs_rq, se);
	se->on_rq = 0;
	account_entity_dequeue(cfs_rq, se);

	/*
	 * 重新规范化vruntime
	 */
	if (!(flags & DEQUEUE_SLEEP))
		se->vruntime -= cfs_rq->min_vruntime;

	/* return excess runtime on last dequeue */
	return_cfs_rq_runtime(cfs_rq);

	update_cfs_group(se);

	/*
	 * Now advance min_vruntime if @se was the entity holding it back,
	 * except when: DEQUEUE_SAVE && !DEQUEUE_MOVE, in this case we'll be
	 * put back on, and if we advance min_vruntime, we'll be placed back
	 * further than we started -- ie. we'll be penalized.
	 */
	if ((flags & (DEQUEUE_SAVE | DEQUEUE_MOVE)) == DEQUEUE_SAVE)
		update_min_vruntime(cfs_rq);
}

和插入一样,实际对树节点操作的工作由实现:

static void __dequeue_entity(struct cfs_rq *cfs_rq, struct sched_entity *se)
{
	rb_erase_cached(&se->run_node, &cfs_rq->tasks_timeline);
}

可以看到删除一个节点要比插入简单的多,这得益于红黑树本身实现的函数。

调度器入口

正如上文所述,每当要发生进程的调度时,是有一个统一的入口,从该入口选择真正需要调用的调度类。

这个入口是内核中一个名为的函数,它会找到一个最高优先级的调度类,这个调度类拥有自己的可运行队列,然后向其询问下一个要运行的进程是谁。

这个函数中唯一重要的事情是执行了这个函数(定义在中),它以优先级为顺序,依次检查每一个调度类,并且从最高优先级的调度类中选择最高优先级的进程。

static inline struct task_struct *
pick_next_task(struct rq *rq, struct task_struct *prev, struct rq_flags *rf)
{
	const struct sched_class *class;
	struct task_struct *p;

	/*
	 * 优化:如果当前所有要调度的进程都是普通进程,那么就直接采用普通进程的调度类(CFS)
	 */
	if (likely((prev->sched_class == &idle_sched_class ||
		    prev->sched_class == &fair_sched_class) &&
		   rq->nr_running == rq->cfs.h_nr_running)) {

		p = fair_sched_class.pick_next_task(rq, prev, rf);
		if (unlikely(p == RETRY_TASK))
			goto again;

		/* Assumes fair_sched_class->next == idle_sched_class */
		if (unlikely(!p))
			p = idle_sched_class.pick_next_task(rq, prev, rf);

		return p;
	}

// 遍历调度类
again:
	for_each_class(class) {
		p = class->pick_next_task(rq, prev, rf);
		if (p) {
			if (unlikely(p == RETRY_TASK))
				goto again;
			return p;
		}
	}

	/* The idle class should always have a runnable task: */
	BUG();
}

每个调度类都实现了方法,它会返回下一个可运行进程的指针,没有则返回NULL。调度器入口从第一个返回非NULL的类中选择下一个可运行进程。

睡眠和唤醒

睡眠和唤醒的流程在linux中是这样的:

  • 睡眠:进程将自己标记成休眠状态,然后从可执行红黑树中移除,放入等待队列,然后调用选择和执行一个其他进程。
  • 唤醒:进程被设置为可执行状态,然后从等待队列移到可执行红黑树中去。

休眠在Linux中有两种状态,一种会忽略信号,一种则会在收到信号的时候被唤醒并响应。不过这两种状态的进程是处于同一个等待队列上的。

1.等待队列

和可运行队列的复杂结构不同,等待队列在linux中的实现只是一个简单的链表。所有有关等待队列的数据结构被定义在中,具体的实现代码则被定义在中。

内核使用结构来表示一个等待队列,它其实就是一个链表的头节点,但是加入了一个自旋锁来保持一致性(等待队列在中断时可以被随时修改)

struct wait_queue_head {
	spinlock_t		lock;
	struct list_head	head;
};
typedef struct wait_queue_head wait_queue_head_t;

而休眠的过程需要进程自己把自己加入到一个等待队列中,这可以使用内核所提供的、推荐的函数来实现。

一个可能的流程如下:

  1. 调用宏创建一个等待队列的项(链表的节点)
  2. 调用把自己加到队列中去。该队列会在进程等待的条件满足时唤醒它,当然唤醒的具体操作需要进程自己定义好(你可以理解为一个回调)
  3. 调用方法把自己的状态变更为上面说到的两种休眠状态中的其中一种。

下面是上述提到的方法的源码:

void add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
	unsigned long flags;

	wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
	spin_lock_irqsave(&wq_head->lock, flags);
	__add_wait_queue(wq_head, wq_entry);
	spin_unlock_irqrestore(&wq_head->lock, flags);
}

static inline void __add_wait_queue(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry)
{
	list_add(&wq_entry->entry, &wq_head->head);
}
void
prepare_to_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state)
{
	unsigned long flags;

	wq_entry->flags &= ~WQ_FLAG_EXCLUSIVE;
	spin_lock_irqsave(&wq_head->lock, flags);
	if (list_empty(&wq_entry->entry))
		__add_wait_queue(wq_head, wq_entry);
	// 标记自己的进程状态
	set_current_state(state);
	spin_unlock_irqrestore(&wq_head->lock, flags);
}

2.唤醒

唤醒操作主要通过实现,它会唤醒指定等待队列上的所有进程。内部由函数将对应的进程标记为状态,接着调用将进程加入红黑树中。

系函数由宏定义,一般具体内部由下面这个函数实现:

/*
 * The core wakeup function. Non-exclusive wakeups (nr_exclusive == 0) just
 * wake everything up. If it's an exclusive wakeup (nr_exclusive == small +ve
 * number) then we wake all the non-exclusive tasks and one exclusive task.
 *
 * There are circumstances in which we can try to wake a task which has already
 * started to run but is not in state TASK_RUNNING. try_to_wake_up() returns
 * zero in this (rare) case, and we handle it by continuing to scan the queue.
 */
static int __wake_up_common(struct wait_queue_head *wq_head, unsigned int mode,
			int nr_exclusive, int wake_flags, void *key,
			wait_queue_entry_t *bookmark)
{
	wait_queue_entry_t *curr, *next;
	int cnt = 0;

	if (bookmark && (bookmark->flags & WQ_FLAG_BOOKMARK)) {
		curr = list_next_entry(bookmark, entry);

		list_del(&bookmark->entry);
		bookmark->flags = 0;
	} else
		curr = list_first_entry(&wq_head->head, wait_queue_entry_t, entry);

	if (&curr->entry == &wq_head->head)
		return nr_exclusive;

	list_for_each_entry_safe_from(curr, next, &wq_head->head, entry) {
		unsigned flags = curr->flags;
		int ret;

		if (flags & WQ_FLAG_BOOKMARK)
			continue;

		ret = curr->func(curr, mode, wake_flags, key);
		if (ret < 0)
			break;
		if (ret && (flags & WQ_FLAG_EXCLUSIVE) && !--nr_exclusive)
			break;

		if (bookmark && (++cnt > WAITQUEUE_WALK_BREAK_CNT) &&
				(&next->entry != &wq_head->head)) {
			bookmark->flags = WQ_FLAG_BOOKMARK;
			list_add_tail(&bookmark->entry, &next->entry);
			break;
		}
	}
	return nr_exclusive;
}

抢占与上下文切换

上下文切换

上下文切换是指从一个可执行进程切换到另一个可执行进程。由定义在中实现:

static __always_inline struct rq *
context_switch(struct rq *rq, struct task_struct *prev,
	       struct task_struct *next, struct rq_flags *rf)
{
	struct mm_struct *mm, *oldmm;

	prepare_task_switch(rq, prev, next);

	mm = next->mm;
	oldmm = prev->active_mm;
	/*
	 * For paravirt, this is coupled with an exit in switch_to to
	 * combine the page table reload and the switch backend into
	 * one hypercall.
	 */
	arch_start_context_switch(prev);
	
	// 把虚拟内存从上一个内存映射切换到新进程中
	if (!mm) {
		next->active_mm = oldmm;
		mmgrab(oldmm);
		enter_lazy_tlb(oldmm, next);
	} else
		switch_mm_irqs_off(oldmm, mm, next);

	if (!prev->mm) {
		prev->active_mm = NULL;
		rq->prev_mm = oldmm;
	}

	rq->clock_update_flags &= ~(RQCF_ACT_SKIP|RQCF_REQ_SKIP);

	/*
	 * Since the runqueue lock will be released by the next
	 * task (which is an invalid locking op but in the case
	 * of the scheduler it's an obvious special-case), so we
	 * do an early lockdep release here:
	 */
	rq_unpin_lock(rq, rf);
	spin_release(&rq->lock.dep_map, 1, _THIS_IP_);

	/* Here we just switch the register state and the stack. */
	// 切换处理器状态到新进程,这包括保存、恢复寄存器和栈的相关信息 
	switch_to(prev, next, prev);
	barrier();

	return finish_task_switch(prev);
}

上下文切换由函数在切换进程时调用。但是内核必须知道什么时候调用,如果只靠用户代码显式地调用,代码可能会永远地执行下去。

为此,内核为每个进程设置了一个标志来表明是否需要重新执行一次调度,当某个进程应该被抢占时,会设置这个标志,当一个优先级高的进程进入可执行状态的时候,也会设置这个标志位,内核检查到此标志位就会调用重新进行调度。

用户抢占

内核即将返回用户空间的时候,如果标志位被设置,会导致被调用,此时就发生了用户抢占。意思是说,既然要重新进行调度,那么可以继续执行进入内核态之前的那个进程,也完全可以重新选择另一个进程来运行,所以如果设置了,内核就会选择一个更合适的进程投入运行。

简单来说有以下两种情况会发生用户抢占:

  • 从系统调用返回用户空间
  • 从中断处理程序返回用户空间

内核抢占

Linux和其他大部分的Unix变体操作系统不同的是,它支持完整的内核抢占。

不支持内核抢占的系统意味着:内核代码可以一直执行直到它完成为止,内核级的任务执行时无法重新调度,各个任务是以协作方式工作的,并不存在抢占的可能性。

在Linux中,只要重新调度是安全的,内核就可以在任何时间抢占正在执行的任务,这个安全是指,只要没有持有锁,就可以进行抢占。

为了支持内核抢占,Linux做出了如下的变动:

  • 为每个进程的引入了计数器,用于记录持有锁的数量,当它为0的时候就意味着这个进程是可以被抢占的。
  • 从中断返回内核空间的时候,会检查和的值,如果被标记,并且为0,就意味着有一个更需要调度的进程需要被调度,而且当前情况是安全的,可以进行抢占,那么此时调度程序就会被调用。

除了响应中断后返回,还有一种情况会发生内核抢占,那就是内核中的进程由于阻塞等原因显式地调用来进行显式地内核抢占:当然,这个进程显式地调用调度进程,就意味着它明白自己是可以安全地被抢占的,因此我们不用任何额外的逻辑去检查安全性问题。

下面罗列可能的内核抢占情况:

  • 中断处理正在执行,且返回内核空间之前
  • 内核代码再一次具有可抢占性时
  • 内核中的任务显式地调用
  • 内核中的任务被阻塞

五.对Linux系统进程模型的看法

1. 进程的优点

  • 顺序程序的特点:具有封闭性和可再现性;
  • 程序的并发执行和资源共享。多道程序设计出现后,实现了程序的并发执行和资源共享,提高了系统的效率和系统的资源利用率。

2. 进程的缺点

  • 操作系统调度切换多个线程要比切换调度进程在速度上快的多。而且进程间内存无法共享,通讯也比较麻烦。
  • 线程之间由于共享进程内存空间,所以交换数据非常方便;在创建或撤消进程时,由于系统都要为之分配和回收资源,导致系统的开销明显大于创建或撤消线程时的开销。

进程是一种数据结构,进程的引入为了使程序在多道程序环境下能并发执行。尽管进程只是计算机操作系统中很小的一方面,但它却为了整个系统运行提供了极大地帮助。我们完全可以将进程比拟成我们现今纷繁复杂的世界,我们人类便是一个个程序,如何更好的调整社会朝着安稳和谐快速发展,这是我们要努力的。在如今计算机越来越复杂的情况下,我们急需对进程进行更深入的研究。

六.参考资料

 

LINUX内核调度分析(进程调度):https://segmentfault.com/p/1210000013007102

LINUX进程模型简析:http://www.cnblogs.com/wenbin123/p/8964465.html

task_sturct (Linux 的 PCB):https://blog.csdn.net/sdoyuxuan/article/details/69938743

 

复制代码
struct task_struct {     
        volatile long state;    //进程状态  
        void *stack;            //内存指针  
        atomic_t usage;                  
        unsigned int flags;    //进程标号(进程名字)  
        unsigned int ptrace;                                        
  
        int lock_depth;        //BLK 锁深度  
  
#ifdef CONFIG_SMP  
#ifdef __ARCH_WANT_UNLOCKED_CTXSW            //配置多核多线程   
        int oncpu;  
#endif  
#endif  
  
        int prio, static_prio, normal_prio;  //进程的优先级  
        unsigned int rt_priority;            //实时进程的优先级  
        const struct sched_class *sched_class;   //调度器的指针  
        struct sched_entity se;              //调度器 实例化的对象  
        struct sched_rt_entity rt;          //实时 调度器的一个对象  
  
#ifdef CONFIG_PREEMPT_NOTIFIERS            //配置抢占通知器   
        /* struct preempt_notifier列表 */  
        struct hlist_head preempt_notifiers;     
#endif  
  
        /*fpu_count 里面内容是如果一个浮点运算器被使用,它记录着连续的上下文切换的次数,如果fpu_Count超过一个 
         临界值,不怎么工作的FPU会火力全开以至于当fpu_count超过 256次后才变得闲置下来,为了解决这个问题,FPU 
         仅仅使用一段时间 */                                 
        unsigned char fpu_counter;  //定义 fpu_count   
#ifdef CONFIG_BLK_DEV_IO_TRACE      //配置 BLK 锁开发版的输入输出跟踪器  
       unsigned int btrace_seq;           
#endif  
  
        unsigned int policy;        
        cpumask_t cpus_allowed;  
  
#ifdef CONFIG_TREE_PREEMPT_RCU     //配置抢占树,抢占的结构体的读写机制,即RCU机制。  
        int rcu_read_lock_nesting;  
        char rcu_read_unlock_special;  
        struct rcu_node *rcu_blocked_node;  
        struct list_head rcu_node_entry;  
#endif /* #ifdef CONFIG_TREE_PREEMPT_RCU */  
  
#if defined(CONFIG_SCHEDSTATS) || defined(CONFIG_TASK_DELAY_ACCT)  
        struct sched_info sched_info;          //调度器的状态   
#endif  
  
        struct list_head tasks;  
        struct plist_node pushable_tasks;  
  
        struct mm_struct *mm, *active_mm;     //虚拟地址空间的结构体   
  
                                 //进程退出时getpid 就获取status就是它。  
        int exit_state;                //task 状态 ,正常退出状态   
        int exit_code, exit_signal;   //退出信号  
        int pdeath_signal;    //当成为孤儿进程时发送信号   

        unsigned int personality;   //表明进程的状态     
        unsigned did_exec:1;  
        unsigned in_execve:1; //第一个表已经调过了exec族函数,已经发生了进程的程序替换 第二个代表该进程正在调用execve函数 第三个 正在等待i/o设备 第四个 表示当fork生成子进程时,是否恢复了进程的默认优先级  
        unsigned in_iowait:1;  

        /* 在分叉时恢复默认优先级/策略*/  
        unsigned sched_reset_on_fork:1;  
        pid_t pid;  
        pid_t tgid;  
  
#ifdef CONFIG_CC_STACKPROTECTOR        //配置堆栈保护措施  
        unsigned long stack_canary;     //canary值 保护编译器 防止堆栈溢出 导致的返回地址被填充   
#endif   
        struct task_struct *real_parent; 
        struct task_struct *parent;  
        struct list_head children;         //子节点和兄弟节点的定义  
        struct list_head sibling;       
        struct task_struct *group_leader;  //线程组的头结点  
       
        struct list_head ptraced;                  //跟踪器的头结点,跟踪器 跟踪 进程的逻辑流,即PC指令流  
        struct list_head ptrace_entry;              
   
        struct pid_link pids[PIDTYPE_MAX];   //定义 PID_LINK 结构体用它通过PID在哈希散列表中查找相应的task_struct  
        struct list_head thread_group;         //用来保存线程组的PID  
  
        struct completion *vfork_done;            
        int __user *set_child_tid;               //指向用户创造创立的线程的TID号   
        int __user *clear_child_tid;             //指向被清除的线程的TID号   
  
        cputime_t utime, stime, utimescaled, stimescaled;  
        cputime_t gtime;  
        cputime_t prev_utime, prev_stime;  
        unsigned long nvcsw, nivcsw;   //上下文切换的次数   
        struct timespec start_time;             
        struct timespec real_start_time;        
        unsigned long min_flt, maj_flt;  
  
        struct task_cputime cputime_expires;  
        struct list_head cpu_timers[3];  
  
        const struct cred *real_cred;   
        const struct cred *cred;         
        struct mutex cred_guard_mutex;  
        struct cred *replacement_session_keyring; 
  
        char comm[TASK_COMM_LEN]; 
                         //文件系统信息  
        int link_count, total_link_count;  
#ifdef CONFIG_SYSVIPC        //配置进程的通信机制  
 
        struct sysv_sem sysvsem;  
#endif  
#ifdef CONFIG_DETECT_HUNG_TASK   
        unsigned long last_switch_count;  
#endif       
        struct thread_struct thread;       //CPU特殊状态的测试,线程结构体   
        struct fs_struct *fs;    //fs 指向一个文件系统信息结构体,该结构体有文件系统的信息  
     //指向记录打开文件信息的 结构体   
        struct files_struct *files;  
        //命名空间的定义   
        struct nsproxy *nsproxy;  
        //配置进程的信号处理    
        struct signal_struct *signal;   //以下是普通信号部分  
        struct sighand_struct *sighand; //这个指向 handler表   
  
        sigset_t blocked, real_blocked;  //这个表示进程的屏蔽字  
        sigset_t saved_sigmask;  
        struct sigpending pending; //pending表  
  
        unsigned long sas_ss_sp;  // 以下是实时信号部分  
        size_t sas_ss_size;  
        int (*notifier)(void *priv);  
        void *notifier_data;  
        sigset_t *notifier_mask;  
        struct audit_context *audit_context;  
#ifdef CONFIG_AUDITSYSCALL   // 配置系统调用   
        uid_t loginuid;  
        unsigned int sessionid;  
#endif  
        seccomp_t seccomp;  
  
#ifdef CONFIG_UTRACE  
        struct utrace *utrace;  
        unsigned long utrace_flags;  
#endif  
        u32 parent_exec_id;  
        u32 self_exec_id;  
/* 配置器保护措施配置  */
        spinlock_t alloc_lock;  
  
#ifdef CONFIG_GENERIC_HARDIRQS  
        struct irqaction *irqaction;  
#endif  
        spinlock_t pi_lock;  
  
#ifdef CONFIG_RT_MUTEXES  // 互斥的配置    
        struct plist_head pi_waiters;  
        struct rt_mutex_waiter *pi_blocked_on;  
#endif  
#ifdef CONFIG_LOCKDEP                 // 死锁模块的配置   
# define MAX_LOCK_DEPTH 48UL  
        u64 curr_chain_key;  
        int lockdep_depth;  
        unsigned int lockdep_recursion;  
        struct held_lock held_locks[MAX_LOCK_DEPTH];  
        gfp_t lockdep_reclaim_gfp;  
#endif  
  
   // 文件系统的日志信息  
        void *journal_info;          
        struct bio *bio_list, **bio_tail;  
   
//VM 虚拟机的状态   
        struct reclaim_state *reclaim_state;  
  
        struct backing_dev_info *backing_dev_info;  
  
        struct io_context *io_context;  
  
        unsigned long ptrace_message;  
        siginfo_t *last_siginfo; 
        struct task_io_accounting ioac;  
#ifdef CONFIG_CPUSETS  
       nodemask_t mems_allowed;      //定义一个结构体 标志 内存是否允许访问 保护配置器的锁的  
#ifndef __GENKSYMS__   
        unsigned short cpuset_mem_spread_rotor;  
        unsigned short cpuset_slab_spread_rotor;  
        int mems_allowed_change_disable;  
#else  
        int cpuset_mem_spread_rotor;  
        int cpuset_slab_spread_rotor;  
#endif  
#endif  
#ifdef CONFIG_CGROUPS // 配置控制组信息   
        struct css_set *cgroups;  
        struct list_head cg_list;  
#endif 
#endif  
};  

猜你喜欢

转载自www.cnblogs.com/dty-no1/p/8977575.html