1.进程
1.1进程的概念
计算机上所有可运行的软件,通常也包括操作系统,被组织成若干顺序进程(sequential process),简称进程(process)。
一个进程就是一个正在执行程序的实例,包括程序设计器、寄存器和变量的当前值。一个进程是某种类型的一种活动,它有程序、输入、输出以及状态。单个处理器可以被若干进程共享,它使用某种调度算法决定何时停止一个进程的工作,并转而为了另一个进程服务功能。
1.2进程的特征
动态性:进程的实质是程序在多道程序系统中的一次执行过程,进程是动态产生,动态消亡的。
并发性:任何进程都可以同其他进程一起并发执行
独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位;
异步性:由于进程间的相互制约,使进程具有执行的间断性,即进程按各自独立的、不可预知的速度向前推进
结构特征:进程由程序、数据和进程控制块三部分组成。
多个不同的进程可以包含相同的程序:一个程序在不同的数据集里就构成不同的进程,能得到不同的结果;但是执行过程中,程序不能发生改变。
2.进程的组织
2.1进程的控制块(PCB)
Linux系统中主要的活动实体就是进程。
每个进程执行一段独立的程序并且在进程初始化的时候拥有一个独立的控制线程。换句话说,每一个进程都拥有一个独立的程序计数器,用这个这个程序计数器可以追踪下一条将要被执行的指令。
所有的进程都被放在一个叫做进程控制块(PCB),的数据结构中,可以理解为进程属性的集合,该控制块由操作系统创建和管理。每个进程在内核中都有一个进程控制块来维护进程相关的信息,Linux内核的进程控制块是(task_struct)结构体。
2.2进程的标识符(PID)
pid_t pid; //内核中用以标识进程的id pid_t tgid; //用来实现线程机制
struct pid { atomic_t count; unsigned int level; /* lists of tasks that use this pid */ struct hlist_head tasks[PIDTYPE_MAX]; struct rcu_head rcu; struct upid numbers[1]; };
每个进程都有一个唯一的标识符(PID),内核通过这个标识符来识别不同的进程,同时,进程标识符(PID)也是内核提供给用户程序的接口,用户程序通过PID对进程发号施令。
PID是32位的无符号整数,它被顺序编号:新创建进程的PID通常是前一个进程的PID加1。然而,为了与16位硬件平台的传统Linux系统保持兼容,在Linux上允许的最大PID号是32767,当内核在系统中创建第32768个进程时,就必须重新开始使用已闲置的PID号。在64位系统中,PID可扩展到4194303。
2.3Linux系统组织进程
- Linux通过
task_struct
结构体来描述一个进程的所有信息,结构体被定义在include/linux/sched.h
中。 - task_struct结构体即是PCB。PCB是进程的唯一标识,PCB由链表实现(为了动态插入和删除)。进程创建时,为该进程生成一个PCB;进程终止时,回收PCB。
- 部分代码:
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 };
3.进程的状态
3.1进程的六种状态
#define TASK_RUNNING
1.表示进程要么正在执行,要么正在准备执行。
#define TASK_INTERRUPTIBLE
2.表示进程被阻塞(睡眠),只有当某个条件是TRUE时,其状态相应的设置为 TASK_RUNNING。它可以被信号和wake_up唤醒。
#define TASK_UNINTERRUPTIBLE
3.表示进程被阻塞(睡眠),只有当某个条件是TRUE时,其状态相应的设置为 TASK_RUNNING。它只能被wake_up唤醒。
#define TASK_STOPPED
4.表示进程被停止执行。
#define TASK_TRACED
5.表示进程被debugger等进程监视着。
#define EXIT_ZOMBIE
6.表示进程的执行被终止,但是其父进程还没有使用wait()等系统调用来获知它的终止信息。
#define EXIT_DEAD
7.表示进程的最终状态。
3.2状态转换图
4.进程的调度
4.1Linux下的O(1)调度算法
4.1.1O(1)调度器
在O(1)调度中,要问最重要的数据结构是运行队列。运行队列描绘了进程队列的结构,在内核源码中用runqueue结构体表示。
struct runqueue { unsigned long nr_running; task_t *curr; prio_array_t *active,*expired,arrays[2]; };
4.1.2优先级数组
O(1)算法的另一个核心数据结构即为prio_array结构体。该结构体中有一个用来表示进程动态优先级的数组queue,它包含了每一种优先级进程所形成的链表。
#define MAX_USER_RT_PRIO 100 #define MAX_RT_PRIO MAX_USER_RT_PRIO #define MAX_PRIO (MAX_RT_PRIO + 40) typedef struct prio_array prio_array_t; struct prio_array { unsigned int nr_active; unsigned long bitmap[BITMAP_SIZE]; struct list_head queue[MAX_PRIO]; };
4.1.3静态优先级和动态优先级
进程有两个优先级,一个是静态优先级,一个是动态优先级.静态优先级是用来计算进程运行的时间片长度的,动态优先级是在调度器进行调度时用到的,调度器每次都选取动态优先级最高的进程运行.
静态优先级的计算: nice值和静态优先级之间的关系是:静态优先级=100+nice+20 而nice值的范围是-20~19,所以普通进程的静态优先级的范围是100~139
动态优先级的计算: 动态优先级=max(100 , min(静态优先级 – bonus + 5 , 139))
4.1.4时间片
O(1)算法采用过期进程数组和活跃进程数组解决以往调度算法所带来的O(n)复杂度问题。过期数组中的进程都已经用完了时间片,而活跃数组的进程还拥有时间片。当一个进程用完自己的时间片后,它就被移动到过期进程数组中,同时这个过期进程在被移动之前就已经计算好了新的时间片。可以看到O(1)调度算法是采用分散计算时间片的方法,并不像以往算法中集中为所有可运行进程重新计算时间片。当活跃进程数组中没有任何进程时,说明此时所有可运行的进程都用完了自己的时间片。那么此时只需要交换一下两个数组即可将过期进程切换为活跃进程,进而继续被调度程序所调度。两个数组之间的切换其实就是指针之间的交换,因此花费的时间是恒定的。
struct prop_array *array = rq->active; if (array->nr_active != 0) { rq->active = rq->expired; rq->expired = array; }
上面的代码说明了两个数组之间的交换,通过分散计算时间片、交换过期和活跃两个进程集合的方法可以使得O(1)算法在恒定的时间内为每个进程重新计算好时间片。
进程运行的时间片长度的计算 静态优先级<120,基本时间片=max((140-静态优先级)*20, MIN_TIMESLICE) 静态优先级>=120,基本时间片=max((140-静态优先级)*5, MIN_TIMESLICE)
4.1.5调度算法
在每次进程切换时,内核依次扫描就绪队列上的每一个进程,计算每个进程的优先级,再选择出优先级最高的进程来运行;尽管这个算法理解简单,但是它花费在选择优先级最高进程上的时间却不容忽视。系统中可运行的进程越多,花费的时间就越大,时间复杂度为O(n)。
//伪代码 for (系统中的每个进程) { 重新计算时间片; 重新计算优先级; }
4.2CFS调度器
CFS 背后的主要想法是维护为任务提供处理器时间方面的平衡(公平性)。这意味着应给进程分配相当数量的处理器。分给某个任务的时间失去平衡时(意味着一个或多个任务相对于其他任务而言未被给予相当数量的时间),应给失去平衡的任务分配时间,让其执行。
要实现平衡,CFS 在叫做虚拟运行时的地方维持提供给某个任务的时间量。任务的虚拟运行时越小, 意味着任务被允许访问服务器的时间越短 — 其对处理器的需求越高。CFS 还包含睡眠公平概念以便确保那些目前没有运行的 任务(例如,等待 I/O)在其最终需要时获得相当份额的处理器。
但是与之前的 Linux 调度器不同,它没有将任务维护在运行队列中,CFS 维护了一个以时间为顺序的红黑树(如下图)。 红黑树是一个树,具有很多有趣、有用的属性。首先,它是自平衡的,这意味着树上没有路径比任何其他路径长两倍以上。 第二,树上的运行按 O(log n) 时间发生(其中 n 是树中节点的数量)。这意味着可以快速高效地插入或删除任务。
任务存储在以时间为顺序的红黑树中(由 sched_entity
对象表示),对处理器需求最多的任务 (最低虚拟运行时)存储在树的左侧,处理器需求最少的任务(最高虚拟运行时)存储在树的右侧。 为了公平,调度器然后选取红黑树最左端的节点调度为下一个以便保持公平性。任务通过将其运行时间添加到虚拟运行时, 说明其占用 CPU 的时间,然后如果可运行,再插回到树中。这样,树左侧的任务就被给予时间运行了,树的内容从右侧迁移到左侧以保持公平。 因此,每个可运行的任务都会追赶其他任务以维持整个可运行任务集合的执行平衡。
- task_struct任务结构和红黑树的结构层次
树的根通过 rb_root
元素通过 cfs_rq
结构(在 ./kernel/sched.c 中)引用。红黑树的叶子不包含信息,但是内部节点代表一个或多个可运行的任务。红黑树的每个节点都由 rb_node
表示,它只包含子引用和父对象的颜色。 rb_node
包含在 sched_entity
结构中,该结构包含 rb_node
引用、负载权重以及各种统计数据。最重要的是,sched_entity
包含 vruntime
(64 位字段),它表示任务运行的时间量,并作为红黑树的索引。 最后,task_struct
位于顶端,它完整地描述任务并包含 sched_entity
结构。
就 CFS 部分而言,调度函数非常简单。 在 ./kernel/sched.c 中,有通用 schedule()
函数,它会先抢占当前运行任务(除非它通过 yield()
代码先抢占自己)。注意 CFS 没有真正的时间切片概念用于抢占,因为抢占时间是可变的。 当前运行任务(现在被抢占的任务)通过对 put_prev_task
调用(通过调度类)返回到红黑树。 当schedule
函数开始确定下一个要调度的任务时,它会调用 pick_next_task
函数。此函数也是通用的(在 ./kernel/sched.c 中),但它会通过调度器类调用 CFS 调度器。 CFS 中的 pick_next_task
函数可以在 ./kernel/sched_fair.c(称为 pick_next_task_fair()
)中找到。 此函数只是从红黑树中获取最左端的任务并返回相关 sched_entity
。通过此引用,一个简单的 task_of()
调用确定返回的 task_struct
引用。通用调度器最后为此任务提供处理器。
- 优先级和CFS:CFS 不直接使用优先级而是将其用作允许任务执行的时间的衰减系数。 低优先级任务具有更高的衰减系数,而高优先级任务具有较低的衰减系数。 这意味着与高优先级任务相比,低优先级任务允许任务执行的时间消耗得更快。 这是一个绝妙的解决方案,可以避免维护按优先级调度的运行队列。
- 跟CFS有关进程:
- 创建新进程: 创建新进程时, 需要设置新进程的vruntime值以及将新进程加入红黑树中. 并判断是否需要抢占当前进程。
- 进程唤醒: 唤醒进程时, 需要调整睡眠进程的vruntime值, 并且将睡眠进程加入红黑树中. 并判断是否需要抢占当前进程。
- 进程的调度: 进程调度时, 需要把当前进程加入红黑树中, 还要从红黑树中挑选出下一个要运行的进程。
- 时钟周期中断: 在时钟中断周期函数中, 需要更新当前运行进程的vruntime值, 并判断是否需要抢占当前进程。
- 部分代码:
完全公平运行队列:描述运行在同一个cpu上的处于TASK_RUNNING状态的普通进程的各种运行信息
struct cfs_rq { struct load_weight load; //运行队列总的进程权重 unsigned int nr_running, h_nr_running; //进程的个数 u64 exec_clock; //运行的时钟 u64 min_vruntime; //该cpu运行队列的vruntime推进值, 一般是红黑树中最小的vruntime值 struct rb_root tasks_timeline; //红黑树的根结点 struct rb_node *rb_leftmost; //指向vruntime值最小的结点 //当前运行进程, 下一个将要调度的进程, 马上要抢占的进程, struct sched_entity *curr, *next, *last, *skip; struct rq *rq; //系统中有普通进程的运行队列, 实时进程的运行队列, 这些队列都包含在rq运行队列中 ... };
调度实体:记录一个进程的运行状态信息
struct sched_entity { struct load_weight load; //进程的权重 struct rb_node run_node; //运行队列中的红黑树结点 struct list_head group_node; //与组调度有关 unsigned int on_rq; //进程现在是否处于TASK_RUNNING状态 u64 exec_start; //一个调度tick的开始时间 u64 sum_exec_runtime; //进程从出生开始, 已经运行的实际时间 u64 vruntime; //虚拟运行时间 u64 prev_sum_exec_runtime; //本次调度之前, 进程已经运行的实际时间 struct sched_entity *parent; //组调度中的父进程 struct cfs_rq *cfs_rq; //进程此时在哪个运行队列中 };
创建进程,设置新进程的vruntime值,task_fork_fair()函数部分代码:
static void task_fork_fair(struct task_struct *p) { struct cfs_rq *cfs_rq; struct sched_entity *se = &p->se, *curr; int this_cpu = smp_processor_id(); struct rq *rq = this_rq(); unsigned long flags; raw_spin_lock_irqsave(&rq->lock, flags); update_rq_clock(rq); cfs_rq = task_cfs_rq(current); curr = cfs_rq->curr; rcu_read_lock(); __set_task_cpu(p, this_cpu); //设置新进程在哪个cpu上运行 rcu_read_unlock(); update_curr(cfs_rq); //更新当前进程的vruntime值 if (curr) se->vruntime = curr->vruntime; //先以父进程的vruntime为基础 place_entity(cfs_rq, se, 1); //设置新进程的vruntime值, 1表示是新进程 if (sysctl_sched_child_runs_first && curr && entity_before(curr, se)) { //sysctl_sched_child_runs_first值表示是否设置了让子进程先运行 swap(curr->vruntime, se->vruntime); //当子进程的vruntime值大于父进程的vruntime时, 交换两个进程的vruntime值 resched_task(rq->curr); //设置重新调度标志TIF_NEED_RESCHED } se->vruntime -= cfs_rq->min_vruntime; //防止新进程运行时是在其他cpu上运行的, 这样在加入另一个cfs_rq时再加上另一个cfs_rq队列的min_vruntime值即可(具体可以看enqueue_entity函数) raw_spin_unlock_irqrestore(&rq->lock, flags); }
进程的主动调度函数是schedule():
asmlinkage void __sched schedule(void) { struct task_struct *prev, *next; unsigned long *switch_count; struct rq *rq; int cpu; need_resched: preempt_disable(); //在这里面被抢占可能出现问题,先禁止它! cpu = smp_processor_id(); rq = cpu_rq(cpu); rcu_qsctr_inc(cpu); prev = rq->curr; switch_count = &prev->nivcsw; release_kernel_lock(prev); need_resched_nonpreemptible: spin_lock_irq(&rq->lock); update_rq_clock(rq); clear_tsk_need_resched(prev); //清除需要调度的位 if (prev->state && !(preempt_count() & PREEMPT_ACTIVE)) { if (unlikely(signal_pending_state(prev->state, prev))) prev->state = TASK_RUNNING; else deactivate_task(rq, prev, 1); //出队, 此处主要是把prev->on_rq赋值为0, 因为当前进程本来就没在红黑树中. on_rq为0后, 后面的put_prev_task函数就不会把当前进程加入红黑树了 switch_count = &prev->nvcsw; } if (unlikely(!rq->nr_running)) idle_balance(cpu, rq); prev->sched_class->put_prev_task(rq, prev); //把当前进程加入红黑树中 next = pick_next_task(rq, prev); //从红黑树中挑选出下一个要运行的进程, 并将其设置为当前进程 if (likely(prev != next)) { sched_info_switch(prev, next); rq->nr_switches++; rq->curr = next; ++*switch_count; //完成进程切换 context_switch(rq, prev, next); cpu = smp_processor_id(); rq = cpu_rq(cpu); } else spin_unlock_irq(&rq->lock); if (unlikely(reacquire_kernel_lock(current) < 0)) goto need_resched_nonpreemptible; preempt_enable_no_resched(); //这里新进程也可能有TIF_NEED_RESCHED标志,如果新进程也需要调度则再调度一次 if (unlikely(test_thread_flag(TIF_NEED_RESCHED))) goto need_resched; }
5.对进程的看法
进程是操作系统最核心的概念,这是对正在运行的程序的一个抽象。所有操作系统其他的所有内容都是围绕着进程的概念展开的。进程是操作系统的核心之一,对于 Linux 技术而言,惟一不变的就是永恒的变化,不断追求更优更有效率的算法,让计算机给人们提供更好的服务。我们从进程模型中学习到的理论知识与算法的更新模式等,都应与现实结合,好好实践。
原文地址:https://www.cnblogs.com/linqingsong/p/8977721.html