进程
2012-08-07 01:37 ggzwtj 阅读(1014) 评论(0) 编辑 收藏 举报命名空间:
命名空间提供了虚拟化的一种轻量级形式,使得我们可以从不同的方面来查看运行系统的全部属性。具体的组织方式如下:
- pid:内核对PID的内部表示,不同task_struct中的不同部分可能指向同一个pid;
- upid:特定命名空间中可见的信息;
- pid_namespace:代表了一个命名空间,组织成树形结构,保存了生成唯一PID的数据结构;
- nsproxy:汇集指向特定于子系统的命名空间包装器的指针;
- pid_hash:根据PID和所在的namespace进行散列;
- task_struct:保存一个进程的信息;
从上面的图中可以看到在知道pid,task_struct,nr,ns等不同信息的时候如何找到其他的信息。
创建进程:
在需要创建进程的时候会用到下面的三个系统调用:
- fork:子进程复制了父进程的资源,各自的资源独立;
- vfork:创建的实际是一个线程,因为没有自己的资源;父进程总是在子进程执行完成后才开始执行;
- clone:可以指定与父进程要共享哪些资源;
这三个系统调用对应到内中的代码最终都是do_fork,通过不同的参数来实现不同的功能。该函数执行的流程如下:
- 对参数和资源进行检查;
- 处理ptrace:判断trace的类型:PTRACE_EVENT_FORK、PTRACE_EVENT_VFORK、PTRACE_EVENT_CLONE;如果将要使用ptrace监控新的进程,那么在创建新进程后会立即向其发送SIGSTOP信号;
- copy_process:完成实际的复制进程,只是复制了结构,并不会启动;
- 检查标志位,因为一些标志位之间本来就是矛盾的。
- 复制task_struct、thread_info等:分配新的结构并直接把current中的内容复制过来。
- 为ptrace的返回值(ftrace_ret_stack)分配内存并初始化;
- 检查资源限制;
- 修改进程调度相关的信息:
- 将标志设置为TASK_RUNNING但是并不加入runqueue,这样做是为了保证没有其他人运行该程序并且不会被外部事件叫醒;
- 设置优先级为current->normal_prio;
- 调用copy_XXX函数来根据标志位复制结构,task_struct中有很多指针,如果要和父进程共享数据的话只需要设置指针的值,如果不共享的话则需要分配新的内存并初始化,这里只会复制管数据的管理结构,而实际的数据是通过写时复制来完成;
- 分配并设置pid中的值以及设置进程关系等;
- 确定PID;
- 在设置CLONE_VFORK标志的情况下,初始化vfork的完成处理程序和ptrace标志;
- 唤醒进程,也就是将task_struct添加到调度队列,子进程优先于父进程执行有助于提高效率;
- 返回进程的PID;
内核线程是由内核主动启动的进程,实际上是将内核函数委托给独立的进程,与系统中其他的进程并行执行。执行的任务包括:
- 周期性地将修改的内存页与页源块设备同步;
- 如果内存页很少使用,将其写入交换区;
- 管理延迟动作;
- 实现文件系统的事务日志;
调度器
可以用两种方法来激活调度:
- 直接调度:比如进程打算睡眠或出于其他原因放弃CPU;
- 周期性调度:以固定的频率运行,不时检查是否有必要进行进程切换;
要对进程进行调度,那么进程中就需要保存对应的信息:
- prio和normal_prio:动态优先级;
- static_prio:静态优先级,在进程创建的时候设置的,在运行的时候可以改变;
- rt_priority:实时进程的优先级;
- sched_class:进程所属的调度类;
- sched_entity:调度器不只是能调度进程,还能调度进程组;
- policy:该进程应用的调度策略;
- cpus_allow:用一个位域保存进程可以在哪些处理器上执行;
- run_list和time_slice:用于循环实时调度器,保存各进程的一个运行表和进程可使用CPU的剩余时间段;
struct sched_class { const struct sched_class *next; void (*enqueue_task) (struct rq *rq, struct task_struct *p, int flags); void (*dequeue_task) (struct rq *rq, struct task_struct *p, int flags); void (*yield_task) (struct rq *rq); bool (*yield_to_task) (struct rq *rq, struct task_struct *p, bool preempt); void (*check_preempt_curr) (struct rq *rq, struct task_struct *p, int flags); struct task_struct * (*pick_next_task) (struct rq *rq); void (*put_prev_task) (struct rq *rq, struct task_struct *p); #ifdef CONFIG_SMP int (*select_task_rq)(struct task_struct *p, int sd_flag, int flags); void (*pre_schedule) (struct rq *this_rq, struct task_struct *task); void (*post_schedule) (struct rq *this_rq); void (*task_waking) (struct task_struct *task); void (*task_woken) (struct rq *this_rq, struct task_struct *task); void (*set_cpus_allowed)(struct task_struct *p, const struct cpumask *newmask); void (*rq_online)(struct rq *rq); void (*rq_offline)(struct rq *rq); #endif void (*set_curr_task) (struct rq *rq); void (*task_tick) (struct rq *rq, struct task_struct *p, int queued); void (*task_fork) (struct task_struct *p); void (*switched_from) (struct rq *this_rq, struct task_struct *task); void (*switched_to) (struct rq *this_rq, struct task_struct *task); void (*prio_changed) (struct rq *this_rq, struct task_struct *task, int oldprio); unsigned int (*get_rr_interval) (struct rq *rq, struct task_struct *task); #ifdef CONFIG_FAIR_GROUP_SCHED void (*task_move_group) (struct task_struct *p, int on_rq); #endif };
实现了这些方法就相当于实现了一个调度器。需要了解一下几个概念:
- 虚拟时间:不同的优先级的进程有不同的表,优先级高的表走得慢,优先级低的表走的快;
- 完全公平调度器:总是选择虚拟时间最小的一个来执行;
- 实时调度器:优先级高的会优先执行;
在完全公平调度器中,会保存一个vruntime。在每次更新完正在执行的进程的虚拟执行时间之后,就会设置min_vruntime为max(vruntime,min(curr,leftmost)),这样就能保证vruntime单调递增。内核会保证每个可运行的进程在某个时间间隔内都会执行一次。考虑各个进程的相对权重,将一个周期内的时间在活动进程之间进行分配。
调度器会用红黑树保存所有的需要调度的单位,而当前正在执行的单位不会保存在树中。
在周期性的调度中,如果可执行的任务少于两个,那么什么都不需要做。在有多个任务时,首先要保证没有哪个进程能够比延迟周期中确定的份额运行得更长。这样,如果进程运行时间比期望的事件长,那么通过resched_task发出重新调度的请求。
在主动发起调度的时候,内核会确保被抢占的进程至少已经运行了某一最小时间间隔。新唤醒的进程不一定由完全公平调度器处理,如果新进程是一个实时进程就会立即请求重调度。如果新进程的虚拟运行时间加上最小时间仍然小于当前执行进程的虚拟运行时间则请求重调度。