linux进程调度分析

linxu混混之牢骚

生命的意义就是在于它的变幻,你永远不会知道明天会发生什么事。 珍惜生命。。。。。

 

进程
linux中,进程有自己的独立的用户空间,和自己独立的内核空间(堆栈),使用一个task_struct结构来表示一个进程。一般用fork,vfork等函数建立。

线程:用户线程,内核线程
linux中,用户进程没有自己独立的用户空间,需要和别的线程共享用户空间。有自己独立的内核空间(堆栈),使用一个task_struct结构来表示。 在用户空间的应用程序中使用pthread_creat建立。
linux中,内核线程没有用户空间,他只能运行在内核空间,使用一个task_struct结构来表示。由于没有用户空间故mm_struct *mm==NULL。在内核使用kernel_thread创建。
另外: linux中,将线程称作轻量级进程(lightweight process),内核可以直接进行调度。
在《深入linux内核》书中有个比喻:假设一个象棋程序使用两个线程,其中一个控制图形化棋盘,等待选手移动并显示在屏幕上,另一个考虑棋的下一步移动。 由此可见,当第一个进程在等待的时候,第二个进程应该继续运行。但是如果象棋程序仅是一个单独的进程,第一个线程就不能简单的发出等待用户行为的堵塞系统调用;否则第二个进程也会被堵塞。 所以linux需要内核为别对线程进行单独的调度。
 
计算机语言中说: 进程是系统分配的最少单元,线程是程序运行的最小单元。
 

进程描述符:


每个一个进程,线程都会用一个task_struct结构来描述的,变量很多,挑重要的介绍:

task_struct
{
........
volatile long state;    //进程的状态,赋值范围:
/***************************************
*TASK_RUNNING--表示进程已经就绪,或已经运行。当一个进程被creat的时候就处于这个状态。
*TASK_INTERRRUPTIBLE---处于等待中的进程,当等待条件为真时被唤醒,可以被信号或中断wake。
*TASK_UNINTERRUPTIBLE---处于等待中的进程,当等待条件为真时被唤醒,但不可以被信号或中断唤醒。
*TASK_STOPPED当进程接受到SIGSTOP和SIGTSTP信号时,进程状态变成这个,但当接受到SIGCONT信号后,重新TASK_RUNNING.
*TASK_KILLABLE 相当于TASK_UNINTERRUPTBLE,但是可以被SIGKILL信号唤醒。
*TASK_TRACED处于调试状态的进程。
*TASK_DEAD当进程调用do_exit后,处于该状态。
**************************************/
pid_t pid;  // process ID,进程标识符。每一个进程都对应一个进程标识符,它是int型变量,最大是32767。linux内核使用一个bitmap来标识每一个使用过的processID。具体什么是bitmap可上网查阅。另外:内核也可以通过一个pid来找到相应的task_struct结构,应该是通过hash table的方式。
struct mm_struct *mm; //用户空间的内存描述符指针, 内核进程没有用户空间,该变量为NULL
unsigned int policy;   //进程的调度算法。 从此可以看出,每一个进程都有自己的调度算法,而不是整个kernel一个调度算法。                                                   

/***************************************
*SCHED_NORMAL 普通进程

*SCHED_FIFO 先入先出实时进程

*SCHED_RR 时间片轮转实时进程。
*SCHEN_BATCH 批处理进程.
*TASK_IDLE 只有在进程空闲时才能被调用的进程。
**************************************/

//SCHED_NORMAL SCHEN_BATCH TASK_IDLE是属于CFS调度类的,他的调度算法在kernel/sched_fair.c中实现。

//SCHED_FIFO SCHED_RR属于实时调度类,他的调度算法在kernel/sched_rt.c中实现
int prio; //进程的优先级,范围是0-140. 其中0-100是实时进程的优先级,100-140是非实时进程的优先级。优先级值越小,优先级越高。由此可见,实时进程总比非实时进程优先级高。
int static_prio; //静态优先级,时间片的大小由静态优先级确定。

//应该说进程的动态优先级是用来进程调度时候用的,静态优先级是进程确定时间片用的,比较繁琐,也没有搞的十分清楚。
........
};


当生成一个进程时候,内核分配进程的数据结构,给一个8k的内核空间。其中thread_info结构在最下面,stack是内核空间的栈(用户空间的栈视进程类型来确定)。内核可以简单通过thread_info结构来找到该进程的task_struct结构。(2.6内核和2.4内核的进程内存结构不同)。有个current来表示当前进程的task_struct结构。在kernel中有 #define current (get_current())。
linux进程调度分析_第1张图片

 

进程的调度:

参考国嵌视频教程中的方法,进程的调度研究步骤:

1.调度策略

2.调度时机

3.调度步骤

 

调度的策略:

进程的调度的策略就是按照进程的类型在 task_struct中的unsigned int policy; 变量来表示的。

调度的时机:

调度采用的是调用schedule()函数。调度的时机就是指  调用schedule的时机。

1.主动式:

当进程运行中需要某些资源等时候,资源无法使用,此时进程会suspend起自己并自动放弃CPU,进行调度。

eg:current->state = TASK_INTERRRUPTIBLE;

         schedule();

 2被动式(抢占式)

被动式即为进程正常的运行,被另一个更高优先级的进城抢占了CPU

抢占分为: 用户抢占和内核抢占。 2.4版本中 只支持用户抢占, 2.6的版本中新添加了内核抢占

1. 用户抢占

用户抢占发生在

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

总结为: 当从 内核空间 返回 用户空间 的时候,就会运行schedule()函数,来进行一次进程调度。

ENTRY(ret_to_user)               //从内核空间返回用户空间时候的调用
ret_slow_syscall:
	disable_irq				@ disable interrupts
	ldr	r1, [tsk, #TI_FLAGS]
	tst	r1, #_TIF_WORK_MASK
	bne	work_pending
no_work_pending:
#if defined(CONFIG_IRQSOFF_TRACER)
	asm_trace_hardirqs_on
#endif
	/* perform architecture specific actions before user return */
	arch_ret_to_user r1, lr

	restore_user_regs fast = 0, offset = 0
ENDPROC(ret_to_user)
work_pending:
 tst r1, #_TIF_NEED_RESCHED                //这个是是否需要调度的一个标志
 bne work_resched                          //跳转到work_resched
 tst r1, #_TIF_SIGPENDING|_TIF_NOTIFY_RESUME
 beq no_work_pending
 mov r0, sp    @ 'regs'
 mov r2, why    @ 'syscall'
 tst r1, #_TIF_SIGPENDING  @ delivering a signal?
 movne why, #0    @ prevent further restarts
 bl do_notify_resume
 b ret_slow_syscall  @ Check work again

work_resched:
 bl schedule        //进行调度

但只有 用户中断  有个问题,当进程进到内核空间后,如果一直不出来,那么就不会进行调度(只有在从内核空间返回时候才调度),就会影响系统的实时性。所以增加了内核抢占。

 2。 内核抢占

内核抢占当有进程运行在内核空间的时候,发生:

  • 内核中断,返回到内核空间时候
  • 使能内核抢占的时候(锁,使能中断)

就会进行内核抢占。

 

但是在内核中,也有几种情况是不允许内核抢占的:

  • 中断中
  • 软中断中
  • spinlock后
  • 正在执行schedule时

在以上情况下是不可以进行内核抢占的。

在thread_info结构中,有个preempt_count变量,当进入以上情况不允许抢占的时候,preempt_count就加1,当退出时候就减一。在减一的同时进行判定,如果为0,就会调用调度函数。

 

内核中断时候的调度:

__irq_svc:
	
.................
#ifdef CONFIG_PREEMPT
	get_thread_info tsk
	ldr	r8, [tsk, #TI_PREEMPT]		@ get preempt count
	add	r7, r8, #1			@ increment it
	str	r7, [tsk, #TI_PREEMPT]               //preempt_count ++
#endif

	irq_handler                                   //中断处理函数
#ifdef CONFIG_PREEMPT
	str	r8, [tsk, #TI_PREEMPT]		@ restore preempt count
	ldr	r0, [tsk, #TI_FLAGS]		@ get flags
	teq	r8, #0				@ if preempt count != 0
	movne	r0, #0				@ force flags to 0
	tst	r0, #_TIF_NEED_RESCHED               //需要进程调度的标志
	blne	svc_preempt
#endif
#ifdef CONFIG_PREEMPT
svc_preempt:
	mov	r8, lr
1:	bl	preempt_schedule_irq		@ irq en/disable is done inside  //进程调度
	ldr	r0, [tsk, #TI_FLAGS]		@ get new tasks TI_FLAGS
	tst	r0, #_TIF_NEED_RESCHED
	moveq	pc, r8				@ go again
	b	1b
#endif



有个_TIF_NEED_RESCHED 标志,当某个进程的时间片用完,或者 当有个更高优先级的进程进入就绪的时候,会置位这个标志。 由此可见,当需要调度切换进程的时候才会进行调度。

 

解锁等使能内核抢占调度:

spin_unlock > raw_spin_unlock > _raw_spin_unlock > _raw_spin_unlock

static inline void __raw_spin_unlock(raw_spinlock_t *lock)
{
	spin_release(&lock->dep_map, 1, _RET_IP_);
	do_raw_spin_unlock(lock);
	preempt_enable();
}
#define preempt_enable() \
do { \
	preempt_enable_no_resched(); \
	barrier(); \
	preempt_check_resched(); \
} while (0)
#define preempt_check_resched() \
do { \
	if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) \
		preempt_schedule(); \
} while (0)

在enable的时候进行check TIF_NEED_RESCHED是否需要调度,并调度。

调度的步骤:

看schedule函数的code即可。

主要步骤:

1.清理当前运行中的进程

2.选择下一个要运行的进程。pick_next_task函数

3.设置新的进程的运行环境

4.进程上下文切换

 

 

总结:

只有以上的时机才会进行进程调度,其他的不会。如:try_to_wake_up函数,仅仅是将一个进程加入到run_queue中去,并在需要时置位need_resched,其实并不会进行任务的调度

 未完待续,只是做了个 国嵌视频的笔记,等以后慢慢学习慢慢润色吧。

你可能感兴趣的:(linux进程调度分析)