LKD:进程管理

进程

 

  1. 进程:处于执行期的程序,包括:可执行程序代码、打开的文件、挂起的信号、内核内部数据、处理器状态、一个或多个具有内存映射的内存地址空间及一个或多个执行线程、用来存放全局变量的数据段等。实际上,进程就是正在执行的程序代码的实时结果。
  2. 执行线程:简称线程,是在进程中活动的对象。每个线程都拥有一个独立的程序计数器、进程栈和一组进程寄存器。内核调度的是线程,而不是进程。
  3. 进程提供两种虚拟机制:虚拟处理器和虚拟内存。
  4. fork():该系统调用通过复制一个现有进程来创建一个全新的进程。调用fork()的进程称为父进程,新产生的进程称为子进程。fork()系统调用从内核返回两次:一次回到父进程,另一次回到新产生的子进程。是由clone()系统调用实现的。
  5. exec():创建新的地址空间,并把新的程序载入其中。
  6. exit():这个函数会终结进程并将其占用的资源释放掉。
  7. wait4():父进程可以通过系统调用查询子进程是否终结。进程退出执行后被设置为僵死状态,直到父进程调用wait()或者waitpid()为止。

 

进程描述符及任务结构

 

  1. 任务队列(task list):内核存放进程的列表,双向循环链表。链表中每一项都是类型为task_struct、称为进程描述符的结构。
  2. task_struct结构是用过slab分配器分配的,这样能达到对象复用和缓存着色的目的。
  3. thread_info():每个任务的thread_info结构在它的内核栈的尾端分配。结构中的task域中存放的是指向该任务实际task_struct的指针

struct thread_info{

        struct task_struct *task;

        …

}

  1. PID:内核通过唯一的进程标识值来标识每个进程。PID是一个数,表示为pid_t类型。为了兼容老版本,PID最大默认值为32768(short int的最大值)。可以通过/proc/sys/kernel/pid_max修改
  2. current宏:是一个全部指针,指向当前进程的struct task_struct结构,即表示当前进程。
  3. current宏的获得:在X86系统上,通过current_thread_info()完成,假定栈的大小为8KB,把后13个有效为屏蔽掉,用来计算出thread_info的偏移。最后,current_thread_info()->task。在PowerPC上,task_struct是保存在一个寄存器中的,current宏只需返回r2寄存器的值。
  4. 进程状态state:

TASK_RUNNING——进程是可执行的;它或者正在执行,或者在运行队列中等待执行。

TASK_INTERRUPTIBLE——进程正在睡眠(它被阻塞),等待某些条件的达成。一旦条件达成,内核就会把进程状态设置为运行。处于此状态的进程也会因为接收到信号而提前被唤醒。

TASK_UNINTERRUPTIBLE——就算是接收到信号也不会被唤醒。这个状态通常在进程必须在等待时不受干扰或等待事件很快就会发生时出现。

_TASK_TRACED

_TASK_STOPPED

进程状态转化:见图3-3

  1. 设置当前进程状态:

set_current_state(state)等价于set_task_state(current, state)

  1. 进程上下文:可执行程序代码从一个可执行文件载入到进程的地址空间执行。一般程序在用户空间执行,当调用了系统调用或者触发了某个异常,他就陷入了内核空间。此时,我们称内核“代表进程执行”并处于进程上下文中。
  2. 进程家族树

所有进程都是PID为1的init进程的后代。内核在系统启动的最后阶段启动init进程。

遍历子进程:

struct task_struct *task;

struct list_head *list;

 

list_for_each(list, &current->children){

        task = list_entry(list, struct task_struct, sibling);

}

 

 进程创建

 

  1. fork()和exec():首先,fork()通过拷贝当前进程创建一个子进程。子进程与父进程的区别仅仅在于PID、PPID和某些资源。exec()函数负责读取可执行文件并将其载入地址空间开始运行。
  2. 写时拷贝:内核此时并不复制整个进程地址空间,而是让父进程和子进程共享同一个拷贝。只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。资源的复制只有在需要写入的时候才进行,在此之前,只是以只读方式共享。这种技术使页的拷贝被推迟到实际发生写入的时候才进行。
  3. fork():fork()——>clone()——>do_fork()——>copy_process()——>dup_task_struct()
  4. 线程在linux的实现:

线程机制提供了在同一程序内共享内存地址空间运行的一组线程。

创建线程:需要在调用clone()的时候传递一些参数标志来指明需要共享的资源

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND, 0);

普通的进程fork()实现:

clone(SIGCHLD, 0);

内核线程:内核线程和普通进程间的区别在于内核线程没有独立的地址空间(实际上指向地址空间的mm指针被设置为NULL)它们只在内核空间运行,从来不切换到用户空间去。ps –ef可以查看

  1. 进程终结

do_exit():释放与进程相关联的资源。进程终结时的清理工作和进程描述符的删除被分开执行。父进程获得已终结子进程的信息后(调用wait),task_struct结构才被释放。

 

 

 

 

进程调度

 

  1. 抢占:有调度程序决定什么时候停止一个进程的运行,以便其他进程能够得到执行机会。这个强制的挂起动作叫做抢占。
  2. 进程的时间片:进程在被抢占之前能够运行的时间是预先设置好的,是分配给每个可运行进程的处理器时间段。
  3. 大部分系统都是抢占式多任务的
  4. 目前的调度算法:完全公平调度算法(CFS),经典的O(1)调度程序
  5. 进程:I/O消耗型和处理器消耗型
  6. 进程优先级

<a>nice值:-20到+19,默认值为0;nice值越大,优先级越低;

   可以通过ps –el查看

<b>实时优先级:0到99,实施优先级数值越高进程优先级越高。任何实时进程的优先级都高于普通的进程。

   可以通过ps –eo status,uid,pid,ppid,rtprio,time,comm

  1. 时间片:是一个数值,它表示进程在被抢占前所能持续运行的时间。
  2. linux操作系统不再通过分配给定的优先级和时间片,而是分配一个给定的处理器使用比。

 

 

Linux调度算法

 

  1. 调度器类:linux调度器是以模块方式提供的,这样允许不同类型的进程可以有针对性地选择调度算法。它允许多种不同的可动态添加的调度算法并存,调度属于自己范畴的进程。CFS是一个针对普通进程的调度类,在linux中称为SCHED_NORMAL
  2. 现代进程调度器有两个通用的概念:进程优先级和时间片。时间片是指进程运行多少时间,进程一旦启动就会有一个默认的时间片。具有更高优先级的进程将运行的更频繁,而且也会被赋予更多的时间片。UNXI系统优先级以nice值形式输出到用户空间,会导致许多反常的问题
  1. 通常的做法是把nice值映射到绝对时长的时间片,这样做会导致进程切换无法最优化。 造成这一问题的原因是我们很难选择一个合适的映射函数。 比如系统中有两个可运行的程序,他们的nice值是0和20,假如说我们给其分配的时间片是100ms和5ms。这样的情况好像很合理没有什么问题。 但是我们按照同样的方式去处理两个低优先级的程序就可能导致进程频繁的切换。 比如现在我们有两个nice值都为20的进程,那么他们各自获得的时间片都是5ms,那么他们将会频繁的进行切换。
  2. 随着nice值增大或减小,其对应的时间片比例差异巨大。 比如nice值为0、1的两个进程,他们获得的时间片是100ms、95ms; 如果他们的nice值是18、19,那么他们获得的时间片会是10ms、5ms,相差一倍。
  3. 绝对时间片必须是定时器节拍的整数倍,其次,系统定时器限制了两个时间片的差异,最后,时间片还会随定时器节拍的改变。这点是引入CFS的唯一原因。

 

上诉问题的实质是:分配绝对的时间片引发的固定的切换频率,给公平性造成了很大的变数。CFS采用的方法是对时间片分配方式进行根本性的重新设计:完全摈弃时间片而是分配给进程一个处理器使用比重。CFS确保进程调度中能有恒定的公平性,而将切换频率置于不断变动中。

 

公平调度

 

  1. CFS的出发点基于一个简单的理念:进程调度的效果应如同系统具备一个理想中的完美多任务处理器。在这种系统中,每个进程将能获得1/n的处理器时间,同时可以给他调度无限小的时间周期。
  2. CFS的做法是:a、允许每个进程允许一段时间、循环轮转、选择运行最少的进程作为下一个运行进程,而不再采用分配给每个进程时间片的做法。

b、CFS在所有可允许进程总数基础上计算出一个进程应该运行多久,而不是依靠nice值来计算时间片。

c、nice值在CFS中被作为进程获得的处理器运行比的权重:越高的nice值获得更低的处理器使用权重,这是相对默认nice值进程而言的。

d、目标延迟:CFS为完美多任务中的无限小调度周期的近似值设立了一个目标。

e、最小粒度:CFS为每个进程获得的时间片底线,默认情况是1ms。当可允许任务数量趋于无限时,它们各自所获得的处理器使用比都趋于0。

  

   总结:任何进程所获得的处理器时间是由它自己和其他所有可运行进程nice值的相对差值决定的。任何nice值对应的绝对时间不再是一个绝对值,而是处理器使用比。

 

 

Linux调度的实现

 

  1. 四个组成部分:时间记账 进程选择 调度器入口 睡眠和唤醒
  2. 时间记账:CFS采用调度器实体结构来追踪进程运行记账

struct sched_entity{

           …

           u64       vruntime;

           …

}

vruntime变量存放进程的虚拟运行时间,该运行时间的计算是经过所有可运行进程总数的标准化(被加权),以ns为单位,CFS使用vruntime变量来记录一个程序到底运行了多长时间以及它还应该再运行多久。

update_curr()函数实现了该记账功能,是由系统定时器周期性调用的,无论是在进程处于可运行态,还是被阻塞处于不可运行态。vruntime可以准确地测量给定进程的运行时间,而且可知道谁应该是下一个被运行的进程。

  1. 进程选择
    1. CFS调度算法的核心:选择具有最小vruntime的任务。
    2. CFS进程选择算法可简单总结为“运行rbtree树中最左边叶子节点所代表的那个进程”,实现函数是_pick_next_entity(),这个函数返回值是CFS调度选择的下一个运行进程。
    3. 向rbtree中加入进程:CFS将进程加入rbtree中, 以及缓存最左叶子节点。这一切发生在进程变为可运行状态(被唤醒)或者通过fork()调用第一次创建进程时。enqueue_entity()函数实现。
    4. 从rbtree中删除进程:删除动作发生在进程阻塞(变为不可运行态)或者终止时。dequeue_entity()函数实现。
  2. 调度器入口

 

进程调度的主要入口点:schedule(),内核其他部分用于调用进程调度器入口,主要调用pick_next_task(),它会按照优先级从高到低检查每一个调度类,从最高优先级的调度类中,选择最高优先级的进程。

 

  1. 睡眠和唤醒
    1. 休眠(被阻塞)的进程处于一个特殊的不可执行状态。
    2. 进程休眠肯定都是为了等待一些事件。内核的操作:进程把自己标记为休眠状态,从可执行红黑树移出,放入等待队列,然后调用schedule()选择和执行一个其他进程。唤醒的过程刚好相反:进程被设置为可执行状态,然后再从等待队列中移到可执行红黑树中。
    3. 休眠有两种相关的进程状态:TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE,区别在于是否响应信号。
    4. 等待队列

等待队列是由等待某些事件发生的进程组成的简单链表,内核用wake_queue_head_t表示。可以用DECLARE_WAITQUEUE()静态创建,也可以由init_waitqueue_head()动态创建。

 

将进程加入到一个等待队列的步骤:

  1. 调用宏DEFINE_WAIT()创建一个等待队列的项。
  2. 调用add_wait_queue()把自己加入到队列中。该队列会在进程等待的条件满足时唤醒它。
  3. 调用prepare_to_wait()方法将进程的状态变更为TASK_INIERRUPTIBLE或者TASK_UNINTERRUPTIBLE。
  4. 如果状态被设置为TASK_INTERRUPTIBLE,则信号唤醒进程。所谓的伪唤醒,因此检查处理信号。
  5. 当进程被唤醒时,它会再次检查条件是否为真,如果是,就退出循环;不是就再次调用schdule()并一直重复这步操作。
  6. 当条件满足后,进程将自己设置为TASK_RUNNING并调用finish_wait()方法把自己移出等待队列。

   

    1. 唤醒

wake_up():它会唤醒指定的等待队列上的所有进程。它调用try_to_wake_up():将进程设置为TASK_RUNNING状态,调用enqueue_task()将此进程放入红黑树中。通常哪段代码促使等待条件达成,它就要负责随后调用wake_up()函数。

 

图4-1描述了每个调度程序状态之间的关系

        

      1. 抢占和上下文切换
    1. 上下文切换:就是从一个可执行进程切换到另一个可执行进程,由context_switch()函数负责。它完成两个基本工作:

        1. 调用switch_mm(),该函数负责把虚拟内存从上一个进程切换到新进程中。
        2. 调用switch_to(),该函数负责从上一个进程的处理器状态切换到新进程的处理器状态,包括保存、恢复栈信息和寄存器信息。
    2. need_resched标志:每个进程都包含一个need_resched标志,表明有其他进程应当被运行,要尽快调用调度程序。接口:

    3. set_tsk_need_resched()
    4. clear_tsk_need_resched()
    5. need_resched()
    6.  

      用户抢占:在以下情况发生

    7. 从系统调用返回用户空间
    8. 从中断处理程序返回用户空间
    9.  

      内核抢占:只要重新调度是安全的,内核就可以进行抢占。什么时候重新调度是安全的,只要没有持有锁,内核就可以进行抢占。锁是非抢占区域的标志。

      preempt_count:每个进程的thread_info中引入,该计数器初始值为0,没当使用锁时加1,释放锁时减1,当数值为0的时候,内核就可执行抢占。

      内核抢占在以下情况发生:

    10. 中断处理程序正在执行,且返回内核空间之前。
    11. 内核代码再一次具有可抢占性的时候
    12. 内核任务显示的调用schedule()
    13. 内核中的任务阻塞(同样会导致调用schedule)
    14.  

       

    15. 实时调度策略
    16. linux提供两种实时调度策略:SCHED_FIFO和SCHED_RR。

       

    17. 与调度相关的系统调用
    18. 见表4-2

 

 

                                                            

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值