Linux内核之进程管理和调度
来源:互联网 发布:后台解析json字符串 编辑:程序博客网 时间:2024/06/05 10:53
管理和调度的任务在于:
1、决定各个进程运行多长时间?何时切换到下一个时间?如何决策下一个进程是哪一个?
2、内核从进程A切换到进程B,再切回进程A时,进程A的执行环境与上一次完全相同
task_struct
内核中使用task_struct的数据结构表示一个进程(include/sched.h)
这个结构体包含了所有特定于其他进程的信息以及关联到整个系统的信息,重要的成员包括:
state :指定了进程的当前状态(就绪、可中断休眠、不可中断休眠、停止)
rlim数组:Linux提供资源限制,通过setrlimit和getrlimits系统函数进行管理修改。 可以通过proc文件系统查看 /proc/self/limits
struct nsproxy *nsproxy:指向一组子命名空间(意味着此进程属于该命名空间)
struct task_struct *group_leader:CLONE_THREAD建立的线程组,线程组中主进程称为组长,其他线程通过group_leader指向组长
pid_t pgrp:进程组组长的PID。使用系统调用setpgrp可以合并独立进程到进程组(进程组简化了向组所有成员发送信号的操作) set_task_pgrp
session:几个进程组可以合并成一个会话。set_task_session
pid_t pid:全局PID
pid_t tgid:全局TGID
struct pid_link pids[PIDTYPE_MAX]:所有共享同一个ID的实例都按进程存储在一个散列表中。(线程组)
struct completion *vfork_done; 支持vfork的完成机制
struct user_struct user; 当前进程的用户 //后来移到 const struct cred *real_cred;/* objective and real subjective task * credentials (COW) */
union thread_union{
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)]
} ; 线程信息和核心态栈共用一个联合体,大部分体系结构使用1-2个内存页来保存thread_union,因此有内存地址对齐的要求。
//调度相关:
unsigned int policy;
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;
struct list_head run_list;
cpumask_t cpus_allowed;
unsigned int timeslice;
//文件系统相关:
struct fs_struct *fs; 文件系统信息
struct files_struct *files 文件描述符信息
struct mm_struct *mm, *active_mm; 虚拟地址空间
命名空间
命名空间创建:fork/clone/unshare
命名空间类型:
struct uts_namespace *uts_ns;struct ipc_namespace *ipc_ns;struct mnt_namespace *mnt_ns;struct pid_namespace *pid_ns;
PID命名空间
简称PID,用fork/clone产生的每个进程都由进程自动地分配一个新的唯一的PID值。PID命名空间是有层次的组织。子命名空间中的PID对于父命名空间可见,但子命名空间无法看到父命名空间的PID。意味着某个进程具有多个PID,凡是可以看到该进程的命名空间,都会为其分配一个PID。
管理PID
目的在于通过PID能够得到task_struct,或者通过task_struct能够得到PID(具体到某个命名空间的PID)
涉及到的数据结构包括:struct pid_namespace, struct upid, struct pid,
涉及到一个全局散列表pid_hash
由PID得到struct task_struct:
1 在pid_hash中查找散列溢出链表, 这个链表的所有节点代表一个struct upid实例。所有这些upid中的ur(即命名空间中的PID)都是相同的。
2 由struct upid通过container_of得到struct pid,由于有可能多个task_struct可能对于一个pid,pid->tasks是这些task_struct链表的头
3 现在我们得到是一个task_struct链表,需要通过task_struct的nsprox比对命名空间,从而确定到底是哪个。
由struct task_struct得到PID:
1 通过task_struct的pid元素得到pid
2 由pid和命名空间得到upid
3 upid得到PID
内核中封装了一些调用函数:pid_nr_ns, pid_nr, pid_vnr, task_pid_nr_ns, task_tgid_nr_ns, find_task_by_pid_ns, find_task_by_vpid,find_task_by_pid等
生成PID
alloc_pidmap
进程管理
fork/vfork/clone
fork建立了父进程的一个完整副本,然后作为子进程执行
vfork不创建父进程的副本,父子进程共享数据
clone产生线程,可以对父子进程之间的共享、复制进程精确控制。通过寄存器参数传递标志,不再复制父进程的栈而是可以指定新的栈地址(可能出现线程与父进程共享地址空间,但线程自身的栈可能在另一个地址空间)。
写时复制(COW)
复制进程时只是复制其页表,同时对父子进程的页表的页进行只读标记。若两个今晨只能读取页内存,那么数据共享不是问题。若其中一个进程试图向复制的内存页写入数据会触发缺页异常,内核会检查是否是COW页,进而创建该页专用于当前进程的副本。
copy_process
fork/vfork/clone最终调用了do_fork,这个函数的主要工作在copy_process。该函数的主要工作包括:dup_task_struct/sched_fork/copy_xxx
dup_task_struct
名字就代表了它的功能,为新的内存/进程申请了内存,并且拷贝了父进程的task_struct副本。 需要注意的是,父子进程的所有数据都相同除了栈信息。task_struct->stack。
sched_task
使用调度器对新进程进行设置,主要初始化一些统计字段,并且在各个CPU之间对可用进程重新均衡。
这需要禁止抢占,然后设置优先级值和调度类,以及作为调度实体中提供给调度类的一些信息。
copy_xxx
若是传入了标志CLONE_XXX,则说明可以和父进程共享XXX的资源,不过需要将XXX的引用技术增加1
pid关系
使用pid_nr得到全局PID,设置pid和tgid。对于tgid,若生成是线程,则需要设置成父进程的tgid。
parent/child关系
if (clone_flags & (CLONE_PARENT|CLONE_THREAD)) {p->real_parent = current->real_parent;p->parent_exec_id = current->parent_exec_id;} else {p->real_parent = current;p->parent_exec_id = current->self_exec_id;}组关系
组成父子关系、线程组、会话组信息。
kernel_thread/kthread_create/kthread_run/kthread_create_cpu
内核线程由内核本身启动的进程。大多数计算机上系统的全部虚拟地址空间分为两个部分:用户层程序访问、内核使用。内核代表用户程序运行时,虚拟地址空间的用户空间部分由mm描述。每当内核执行上下文切换时,虚拟地址空间的用户层部分都会切换,以便与当前运行的进程匹配。
内核线程的mm为空,且进程被称为惰性TLB进程。
启动新程序:execve系统调用
这需要通过新代码替换现存程序,Linux提供了execve系统调用,这个函数的主要工作由do_execve完成。需要打开要执行的文件,分析文件格式,使用对应的处理类处理程序执行。 最关键的时将应用程序映射到虚拟地址空间中。
退出进程:exit系统调用
进程使用exit系统调用终止,它会将各个引用计数减1,如果引用计数器归0而没有进程再使用对应的结构,那么相应的内存区域返回给内存管理模块。
进程调度
目的在于让各个进程尽可能地公平共享CPU时间,同时考虑不同的任务优先级。
内核中存在两个调度器:主调度器/周期性调度器。 这两个调度器称为通用调度器或核心调度器。 调度器使用调度类判断接下来运行哪个进程,在选中将要运行的进程后必须执行底层任务切换
调度相关数据结构
task_struct
这个结构体中表示优先级的有3个元素:static_prio,normal_prio, prio。分别表示静态(启动时分配)、普通(静态优先级+调度策略)、动态优先级(可临时提高)。
struct sched_class
这个结构体提供了通用调度器(核心调度器)和各个调度方法之间的关联。这个结构体包括的操作:
enqueue_task/dequeue_task,操作就绪队列
sched_yield进程放弃对处理器的控制权
check_preemp_cur 用一个新进程来抢占当前进程
pick_next_task选择下一个将要运行的进程
set_curr_task 调度测率发生变化时
task_tick 每次激活周期性调度器时
new_task 用于fork系统调用和调度器之间的联系。
struct rq
内核提供了struct rq数据结构用于表示就绪队列,并为系统所有就绪队列声明了runqueues数组中,该数组每个元素对应一个CPU
static DEFINE_PER_CPU_SHARED_ALIGNED(struct rq,runqueues)就绪队列是全局调度器许多操作的起点,就绪队列中的进程由各个调度器类直接管理(出入队列、查询等)
struct sched_entity
调度器可以操作比进程更一般的实体,这个结构体中比较重要的元素包括:
load: 调度实体的权重,它与就绪队列中的负荷比作为调度算法的重要参考依据
run_node: 有了它就可以将调度实体放入红黑树
on_rq:是否接受调度
sum_exec_runtime/prev_sum_exec_runtime:记录消耗的CPU时间(注意是物理时间)
vruntim:记录消耗的虚拟时间
调度实体的优先级
如何计算优先级
一般情况下,调度器考虑的有效级是prio(动态优先级),计算方法:effective_prio();
计算负荷权重
进程的重要性不仅由优先级指定,而且需要考虑保存在task_struct->se.load的负荷权重。
set_load_weight负责根据进程类型及其静态优先级计算负荷权重。
实时进程的进程的权重是普通进程的两倍(实际上时普通进程最大权重的2倍)
idle进程则使用默认的权重
当进程添加到就绪队列时,内核会调用inc_nr_running,修改就绪队列中的进程数并且累加进程的负荷值。
实际应用中,普通进程的优先级、负荷权重在判断是否抢占时有用,负荷权重在计算应得的运行时间有用。
核心调度器(通用调度器)
调度器的实现基于两个函数:周期性调度器函数和主调度器函数:
scheduler_tick(周期性调度器函数)
1、管理内核中与整个系统和各个进程的调度相关的统计量
2、激活负责当前进程的调度类的周期性调度方法。(rq->curr->sched_class->task_tick)
- switch_mm
- switch_to
#define switch_to(prev, next, last)\do {\__mips_mt_fpaff_switch_to(prev);\if (cpu_has_dsp)\__save_dsp(prev);\__clear_software_ll_bit();\(last) = resume(prev, next, task_thread_info(next));\} while (0)__mips_mt_fpaff_switch_to和__save_dsp与具体mips硬件结构相关,先忽略掉。
/* * task_struct *resume(task_struct *prev, task_struct *next, * struct thread_info *next_ti) */.align5LEAF(resume)mfc0t1, CP0_STATUSLONG_St1, THREAD_STATUS(a0) //存放CP0_STATUScpu_save_nonscratch a0 //存s0-s7 sp fpLONG_Sra, THREAD_REG31(a0) //存ra/* * check if we need to save FPU registers */PTR_Lt3, TASK_THREAD_INFO(a0) //处理FPU寄存器LONG_Lt0, TI_FLAGS(t3)lit1, _TIF_USEDFPUandt2, t0, t1beqzt2, 1f //不需要保存FPUnort1, zero, t1andt0, t0, t1LONG_St0, TI_FLAGS(t3)/* * clear saved user stack CU1 bit */LONG_Lt0, ST_OFF(t3)lit1, ~ST0_CU1andt0, t0, t1LONG_St0, ST_OFF(t3)fpu_save_double a0 t0 t1# c0_status passed in t0# clobbers t11:/* * The order of restoring the registers takes care of the race * updating $28, $29 and kernelsp without disabling ints. */move$28, a2 //<span style="color:#ff0000;">gp存储next_ti指针的值</span>cpu_restore_nonscratch a1 //恢复next的s0-s7 sp fp ra寄存器数据PTR_ADDUt0, $28, _THREAD_SIZE - 32 //获取恢复调度之前上个活动进程设置的next_tiset_saved_spt0, t1, t2#ifdef CONFIG_MIPS_MT_SMTC/* Read-modify-writes of Status must be atomic on a VPE */mfc0t2, CP0_TCSTATUSorit1, t2, TCSTATUS_IXMTmtc0t1, CP0_TCSTATUSandit2, t2, TCSTATUS_IXMT_ehbDMT8# dmtt0movet1,rajalmips_ihbmovera,t1#endif /* CONFIG_MIPS_MT_SMTC */mfc0t1, CP0_STATUS//修改CP0_STATUSlia3, 0xff01andt1, a3LONG_La2, THREAD_STATUS(a1)nora3, $0, a3anda2, a3ora2, t1mtc0a2, CP0_STATUS#ifdef CONFIG_MIPS_MT_SMTC_ehbandit0, t0, VPECONTROL_TEbeqzt0, 1femt1:mfc0t1, CP0_TCSTATUSxorit1, t1, TCSTATUS_IXMTort1, t1, t2mtc0t1, CP0_TCSTATUS_ehb#endif /* CONFIG_MIPS_MT_SMTC */movev0, a0 //返回指针指向a0,即当前调度之前的活动进程jrra //跳转到next保存的ra寄存器指向的地址END(resume)上述代码代码主要实现了寄存器的保存和恢复(task_struct->thread_info)。需要注意的是,保存和恢复的寄存器只局限于S0-S7 ,Sp,FP,RA,CP0等。对于像A->B这样的调度,resume返回值就是A的指针值,利用的是a0-a3参数寄存器不保存的特性。
完全公平调度类
数据结构
调度算法
update_curr函数更新vruntime/min_vruntime
延迟跟踪
enqueue_task_fair
pick_next_task_fair
check_preempt_tick处理周期性调度器 / check_preempt_wakeup 唤醒抢占
task_new_fair新进程处理
实时调度类
调度器增强
- Linux内核之进程管理和调度
- Linux内核设计基础(九)之进程管理和调度
- 深入linux内核架构读书笔记-进程管理和调度
- 深入linux内核架构-第二章-进程管理和调度
- Linux内核之进程调度
- Linux内核之进程调度
- linux内核之进程调度
- Linux内核之进程调度
- Linux内核之内核进程调度
- Linux内核学习之四--进程、进程调度、系统调用、proc文件系统和内核异常分析
- linux内核之进程调度(一)
- linux内核之进程调度(一)
- 内核驱动之Linux进程调度
- LINUX内核设计思想之进程调度
- Linux内核设计与实现-进程管理与进程调度
- Linux嵌入式 -- 内核 - 进程控制 和 调度
- Linux内核之进程管理
- Linux内核之进程管理
- Mysq用户创建、修改密码、授权命令
- 单调队列详解
- mac - 终端常用命令(个人笔记)
- PL/SQL Developer连接本地Oracle 11g 64位数据库
- C# 客户端 在 服务器获取数据库图片
- Linux内核之进程管理和调度
- oracle学习笔记
- hdoj 1535 Invitation Cards 【最短路径SPFA】【正反向建边求单源最短路之和】
- 强制去除xcode的编译警告
- [高可用]Ubuntu搭建高可用的RabbitMQ(2)
- 菜鸟系列——最小生成树
- 数据结构与算法分析(C语言描述)L1.1
- Android _Smali中插入Toast
- 动态编辑删除Toolbar中的元素