dcLunatic's blog

Linux进程管理

字数统计: 12k阅读时长: 41 min
2018/09/30 Share

Linux进程

[TOC]

进程是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。
线程:单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位。
换句话说,进程线程都是对CPU工作时间段的描述(通过task_struct)。

而在Linux系统运行中,程序对应的进程是怎么被创建,被管理,被切换,被调度的呢?

进程的组成,创建,状态机

进程的组成,创建

所有进程都会有一个共同的父进程,PID为1的init(systemd)进程,它是内核引导后启动的第一个进程,主要用来执行一些开机初始化脚本和监视进程,读取配置文件/etc/inittab,永远不会停止。必要的时候可以作为参照。如果某个进程突然成为游离进程(其父进程终止了),此时,这个进程会将init进程作为参照,作为它们的父进程,然后gg。父进程死了它的子进程也会死。可以通过pstree查看系统内正在运行的各个进程的之间的继承关系。
inittab 是一个不可执行的文本文件,每行的基本格式为:id:runlevels:action:process

  • id: 1-2个字符,配置行的唯一标识,不能重复。
  • runlevels: 配置行适用的运行级别,可以填入多个运行级别,如12345或者35。
    • 0: 关机
    • 1: 单用户字符界面
    • 2: 不具备网络文件系统(NFS)功能的多用户文字界面
    • 3: 具备网络功能的多用户字符界面
    • 4: 保留
    • 5: 具有网络功能的图形用户界面
    • 6: 重新启动系统
  • action: 表示init的行为,行为表略。
  • process: 为init执行的进程,这些进程都保存在/etc/rcX.d/中,其中X代表运行级别,rc程序接受参数X,然后执行/etc/rcX.d/下面的程序。
    • 这些程序其实都是符号链接,以S打头的表示启动该程序,而以K打头的标识终止该程序,后面的数字标识执行顺序,越小越先执行,剩下的标识程序名。
    • 系统启动或者切换到该运行级别时会执行以S打头的程序,系统切换到其他运行级别是会终止以K打头的程序。
    • 可以通过chkconfig程序进行管理
      init程序也是一个进程,和普通进程拥有一样的属性,比如修改了/etc/inittab,想要立即生效,可以通过运行kill -SIGHUP 1来实现,也可以通过init q来实现。

kill命令的工作原理是:向Linux系统的内核发送一个系统操作信号和某个程序的进程标识号,然后系统内核就可以对进程标识号指定的进程进行操作。

在Linux内核中,进程通过一个双向链表来进行管理,每一个进程包含一个进程描述符(task_struct)

  • 进程描述符
    • ​当把一个程序加载到内存当中,此时,这个时候就有了进程,关于进程,有一个相关的叫做进程控制块(PCB),这个是系统为了方便进行管理进程所设置的一个数据结构,通过PCB,就可以记录进程的特征以及一些信息。 内核当中使用进程描述符task_struct。 这个task_struct就是一个定义的一个结构体,通过这个结构体,可以对进程的所有的相关的信息进行维护,对进程进行管理。
    • 推荐一篇文章
  • 进程的组成
    • 程序读取的上下文,表示程序读取执行的状态
    • 程序当前执行的目录
    • 程序服务的文件与目录
    • 程序访问的权限
    • 内存和其他分配给进程的系统资源
  • 进程的创建
    • 进程总是通过fork系列的系统调用来创建的,最终都是调用do_fork函数,只是参数上的不同而已

      • fork(do_fork(CLONE_SIGCHLD, …))

        • fork创建一个进程时,子进程只是完全复制父进程的资源,复制出来的子进程有自己的task_struct结构和pid,但却复制父进程其它资源(用户空间、文件描述符集)。
        • 采用cow(copy on write)写时复制来降低系统调用的开销。
        • 一次返回两个值,对于父进程,是子程序的pid,而对于子程序,成功状态下是0。
        • 在fork之后,子进程会获得父进程的数据段,堆栈段的副本,而代码段则是共享的。
      • clone(do_fork(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGCHLD, …))

        • fork()是全部复制,vfork()是共享内存,而clone()是则可以将父进程资源有选择地复制给子进程,前两者都是不带参数的,后者有参数clone_flags。
      • vfork(do_fork(CLONE_VFORK|CLONE_VM|CLONE_SIGCHLD, …))

        • vfork系统调用不同于fork,用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。

        • vfork也是在父进程中返回子进程的进程号,在子进程中返回0。

        • 用 vfork创建子进程后,父进程会被阻塞直到子进程调用exec(exec,将一个新的可执行文件载入到地址空间并执行之)或exit。

        • vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的 ,因此通过vfork共享内存可以减少不必要的开销。

      • 三者的区别

        • 拷贝内容
        • 访问次序控制
          • fork不对父子进程的执行次序进行任何限制,运行顺序由内核的调度算法决定。
          • vfork调用后,子进程先运行,父进程挂起,直到子进程调用了exec或者exit后,父子进程的执行次序才不再有限制。
          • clone中有标志CLONE_VFORK来决定子进程在运行是父进程是阻塞的还是运行的,如果没有设置,则同时进行,设置了,与fork一样。
  • CLONE_VM标识:表示共享地址空间(变量等)

    CLONE_FILES标志:表示共享文件描述符表

    CLONE_VFORK标识:标识父进程会被阻塞,子进程会把父进程的地址空间锁住,直到子进程退出或执行exec时才释放该锁

    SIGCHLD标识:共享信号

  • 内核也可以通过kernel_thread函数来创建内核进程

Linux 进程状态机

进程状态转换关系图

  • 虽然进程的状态有好多种,但是进程状态的变迁只有两个方向,从TASK_RUNNING到非TASK_RUNNING,或者相反。总之,进程状态的转换必然会经过TASK_RUNNING,不可能在两个非RUN状态直接转换。
  • 进程从非TASK_RUNNING状态变为TASK_RUNNING状态,是由别的进程(也可能是中断处理程序)执行唤醒操作来实现的。执行唤醒的进程设置被唤醒进程的状态为TASK_RUNNING,然后将其task_struct结构加入到某个CPU的可执行队列中。于是被唤醒的进程将有机会被调度执行。
  • 进程从TASK_RUNNING状态变为非TASK_RUNNING状态,有两种途径。
    • 响应信号而进入TASK_STOPED状态,或者TASK_DEAD状态
    • 执行程序主动进入TASK_INTERRUPTIBLE状态(如nanosleep调用),或者是TASK_DEAD状态(如exit系统调用),另一方面,也有可能是由于执行系统调用需要的资源得不到满足,而进入TASK_INTERRUPTIBLE状态或TASK_UNINTERRUPTIBLE状态(如select系统调用)。
  • 可以通过ps命令来查看系统中存在的进程,以及它们的状态。
  • 系统中绝大部分进程都处于TASK_INTERRUPTIBLE状态,因为CPU也就那么几个,所以绝大部分进程都处于睡眠状态。
  • TASK_UNINTERRUPTIBLE,不可中断的睡眠状态,指该进程不接受异步信号,如kill -9无法杀死该类进程,这种状态的意义在于,内核中的某些处理流程是不能被打断的,如果响应了异步信号,程序的执行流程中就会给插入了一段处理异步信号的流程(可能只存在于内核态,也可能会延伸到用户态)于是乎,原有的流程就给中断了。 而且,一般系统进程中存在于这个状态的进程是非常少,而且正常来说,都是非常短暂的,基本无法察觉得到的。其实通过vfork调用后,父进程就是处于一种TASK_UNINTERRUPTIBLE的状态,知道子进程调用exec或者exit。
  • TASK_STOPPED 或者 TASK_TRACED,暂停状态或跟踪状态。
    • 向某个进程发送SIGSTOP信号或者SIGCONT信号可以进入TASK_STOPPED或者恢复到TASK_RUNNING。
    • TASK_TRACED状态一般只发生在该进程被调试暂停的时候,该进程停下来,等待跟踪它的进程对其进行操作,TASK_TRACED状态下的进程不响应SIGCONT信号,只能是调试它的进程来恢复它,或者调试进程退出时也会恢复。
  • TASK_DEAD - EXIT_ZOMBIE,退出状态,进程成为僵尸进程。
    • 进程在退出的过程中,处于TASK_DEAD状态,在退出过程中,进程所占有的资源将会被回收,除了task_struct结构(以及少数资源),也就是说,进程表只存在这个进程对应的task_struct这个空壳,所以叫做僵尸,由父进程负责收集,收尸。从这个尸体(task_struct)中可以得到很多有用的信息,如子进程的退出码,一些统计信息(虽然内核也可以吧这些信息保存在其他的地方,但是不这么做)。父进程可以通过wait系列的系统调用来等待子进程的退出,然后获取退出信息,然后wait系列的系统调用会清理尸体(task_struct)。
    • 子进程在退出的时候,内核会给父进程发送一个信号(SIGCHLD)来通知父进程过来收尸。
    • 进程可以通过调用exit来进入该状态。
  • TASK_DEAD - EXIT_DEAD,退出状态,进程即将给销毁。
    • 进程也有可能不会经历TASK_DEAD - EXIT_ZOMBIE状态而直接销毁。如detach过的进程,SIGCHLD信号的handle为SIG_IGN,忽略了SIGCHLD信号。

进程的调度

无论是在批处理系统还是分时系统中,用户进程数一般都多于处理机数、这将导致它们互相争夺处理机。另外,系统进程也同样需要使用处理机。这就要求进程调度程序按一定的策略,动态地把处理机分配给处于就绪队列中的某一个进程,以使之执行。这就是进程调度。

当cpu中正在执行的进程(process A)被移出(suspend),cpu中当前进程的相关信息写到需要移除的进程的进程描述符(task_strut)中,下一个进程(process B)被内核读取(该过程称为resume,恢复)到cpu中, 注意:进程的切换也是必须由内核来执行的。进程的切换就是通过用户空间和内核模式的不断切换来完成的。进程切换=CPU加载上下文+CPU执行+CPU保存上下文

Linux 进程调度的目标

  • 高效性: 高效性意味着在相同时间下要完成更多的任务。调度程序会被频繁的执行,所以调度程序要尽可能的高效。
  • 加强交互性能: 在系统相当的负载下,也要保证系统的响应时间。
  • 保证公平和避免饥渴
  • SMP调度: 调度程序必须支持多处理系统。
  • 软实时调度: 系统必须有效的调用实时进程,但不保证一定满足其要求。

Linux 进程优先级

  1. 普通进程的优先级
    • 使用SCHED_NORMAL调度策略
  2. 实时进程的优先级
    • 使用SCHED_FIFO(一次机会做完) 或者 SCHED_RR(多次轮转) 调度策略

任何时候,实时进程的优先级都高于普通进程,实时进程只会被更高级的实时进程抢占

Linux 进程调度

  • 实时进程调度
    • 实时进程,只有静态优先级,因为内核不会根据休眠等因素对其静态优先级做调整,范围是在0 - MAX_RT_PRIO - 1 之间。而nice值,影响的优先级在MAX_RT_PRIO - MAX_RT_PRIO + 40之间的进程。

    • MAX_RT_PRIO: 默认配置为100.

    • 不同于普通进程,实时进程高的进程总是先于优先级低的进程执行,直到实时优先级高的实时进程无法执行,实时进程总是被认为处于活动状态。

    • 如果有数个进程相同的实时进程,那么系统会按照进程在队列上的顺序选择进程。

    • 例如CPU运行实时进程A的实时优先级为a,而此时有一个实时优先级为b的实时进程B进入可运行状态,那么只要b > a,系统将会中断A的执行,而优先执行B,直到B无法执行。

    • 调度策略
      • FIFO 的进程,意味着只有当前进程执行完毕才会轮到其他进程执行。
      • RR 的进程,内核会为进程分配时间片,一旦时间片消耗完毕,内核会将该进程置于进程队列的末尾,然后运行其他相同优先级别的进程,如果没有其他相同优先级的进程,则该进程会继续执行。

对于实时进程,高优先级别的进程执行到无法执行了,才会轮到低优先级别的进程执行。

  • 非实时进程调度
    • Linux 对于普通进程,根据动态优先级进行调整,而动态优先级是由静态优先级(static_prio)调整而来的。 静态优先级是用户不可见的,隐藏在内核中,而内核会给用户一个可以影响静态优先级的接口,那就是nice值。

    • static_prio = MAX_RT_PRIO + nice + 20

    • nice 值与静态优先级的关系如上所示。nice值的范围是-20 - 19之间,所以静态优先级范围在100 - 139之间。

    • 普通进程的动态优先级越低,优先越高,与实时进程相反,也就是说,nice的值越大,优先级越低。

进程时间片是完全依赖于static_prio 定制的,如下所示

$$
base\ time\ quantum(ms) =
\begin{cases}
(140 - \text{static_prio})\times20, \text{static_prio < 120}\
(140 - \text{static_prio})\times20, \text{static_prio } \ge \text{ 120}
\end{cases}
$$

进程调度的其他因素

系统在调度进程的时候,还会考虑其他因素,进而来计算出一个叫进程动态优先级的东西,基于这个来实施调度。

  • 进程属性
    • 交互式进程(I/O密集型,等待IO,对cpu的需求量小)
    • 批处理进程(CPU密集型)
    • 实时进程
  • 分配策略
    • 不仅要考虑静态优先级,也要考虑进程的属性,如果进程是属于交互式进程,那么可以适当的调高它的优先级,使得界面反应更加迅速,从而提高用户体验。

    • 交互式进程可以从一个平均睡眠时间来进行判断,如果进程过去睡眠的时间越多,则越有可能属于交互式进程。则系统在调度的时候,会给该进程更多的奖励(bonus),以便该进程有更多的时间能够执行。奖励(bonus)的范围为0 - 10。

    • 睡眠和CPU耗时反应了进程IO密集和CPU密集两大瞬时特点,不同时期,一个进程可能即是CPU密集型也是IO密集型进程。对于表现为IO密集的进程,应该经常运行,但每次时间片不要太长。对于表现为CPU密集的进程,CPU不应该让其经常运行,但每次运行时间片要长。交互进程为例,假如之前其其大部分时间在于等待CPU,这时为了调高相应速度,就需要增加奖励分。另一方面,如果此进程总是耗尽每次分配给它的时间片,为了对其他进程公平,就要增加这个进程的惩罚分数。

    • 系统会严格按照动态优先级高低的顺序安排进程执行。动态优先级高的进程进入运行状态,或者时间片消耗完毕后才会轮到动态优先级低的进程执行。

    • 动态优先级的计算主要考虑两个因素

      • 静态优先级(static_prio)
      • 平均睡眠时间(bonus)
    • 计算公式如下
      $$
      \text{dynamic_prio} = max{100,\ min{static_prio- bonus+5,\ 139} }
      $$

    • Linux2.6在计算的时候有空间换时间的策略,从而保证最优进程能够在O(1)的时间内完成。

    • CFS 的virtutime机制。

进程切换 (Context switch)

当cpu中正在执行的进程(process A)被移出(suspend),cpu中当前进程的相关信息写到需要移除的进程的进程描述符(task_strut)中,下一个进程(process B)被内核读取(该过程称为resume,恢复)到cpu中, 注意:进程的切换也是必须由内核来执行的。进程的切换就是通过用户空间和内核模式的不断切换来完成的。进程切换=CPU加载上下文+CPU执行+CPU保存上下文

调度触发的时机

  1. 当前进程进入非可执行状态

  2. 抢占

    • 进程用完了时间片
    • 出现优先级更高的进程,会被影响而唤醒。

    内核在响应时钟中断的过程中,发现当前进程的时间片已经用完

    内核在响应时钟中断的过程中,发现优先级更高的进程所等待的外部资源是可用的,于是唤醒它。

其他相关

内核抢占

  • 理想情况下,只要满足了出现优先级更高的进程这个条件,当前进程就应该被立刻抢占。但是就像多线程程序需要使用锁来保护临界区资源一样,内核中也有很多这样的临界区,来保证一些程序不给抢占。或者是处于效率考虑。
  • Linux2.4不支持内核抢占,进程在内核态是不允许抢占的,只有返回用户态时会触发调整。
  • Linux2.6支持内核抢占,但是很多地方为了保护临界区资源而临时性的禁用抢占。

SMP调度下的负载均衡问题

多处理器下,每个CPU都有对应的可执行队列,但是一个可执行状态的进程同一时刻只能存在于一个可执行队列中。

问题

  • 每个CPU可执行队列中进程数目不均衡的时候,要怎么调整,什么时候算作是不均衡,需要调整?
  • CPU之间的关系,两个CPU之间,是不是相互独立的,cache可以共享吗?或者,是不是由同一个物理CPU通过超线程技术虚拟出来的
  • 反正就是乱七八糟的就是了。。。。。

优先级继承

假如有一个进程A,因为要进入临界区,但是临界区有了B,而在等待睡眠,直到B退出临界区后,进程A才会被唤醒。

可能存在这么一种情况,A的优先级很高,而B的优先级很低,B进入了临界区,但是却给其他优先级高于B低于A的进程C抢占,A就等不到运行,此时叫做优先级反转。此时会通过以下方法解决:在A开始等待的时候,将A的优先级赋给B,直到B退出临界区后恢复其原本的优先级。

这就是优先级继承。

中断处理程序的优化

正常来说,中断处理程序处于一个不可调度的上下文中,CPU响应硬件中断自动跳转到内核设定的中断处理程序去执行,再到处理程序执行完。这整个过程中是不能被抢占的。

一个进程如果被抢占了,可以通过保存在它的进程控制块(task_struct)中的信息,在之后的某个时间恢复它的运行,而中断上下文是没有task_struct的,所以是不可能杯抢占的。

也就是说,中断程序的伪优先级比任何进程要高,但是呢在实际中,某些实时进程会拥有比中断进程更高的优先级。所以在一些系统中,会给中断处理程序赋予task_struct以及优先级。但是会影响一些性能,但是却能保证实时。

调度程序的效率问题

优先级明确了哪个进程应该被调度执行,而调度程序还要关心效率问题,因为会被频繁的执行,所以效率很重要的。

所以,调度程序,如何来获得应该被执行的程序,就是一个问题了。

在Linux2.4,可执行状态的进程会被放在一个链表中,每次调度,扫描整个链表,以找出最优的那个进程来运行,复杂度为O(N)

在Linux2.6早期,可执行状态的进车功能会被挂载N(N=140)个链表中,每一个链表代表一个优先级,系统支持多少个优先级就有多少个链表。每次调度,调度程序只需要从第一个不为空的链表中取出链表头的进程即可。复杂度为O(1).

而在现在Linux2.6的版本中,可执行状态的进程会按照优先级顺序被挂载一个红黑树上,每次调度,找出树中优先级最高的那个进程。复杂度为O(LogN)

只是为了公平

第一种是基于空间换时间的策略,在一组数目不大的链表中来实现的,这样子优先级的取值范围很小(大了会浪费太多空间),区分度不大不能满足公平性的需求,而使用红黑树对优先级的取值没有限制,区分度可以更高。而且在同等情况下,140个优先级,O(LogN)的复杂度也不会太差。

处理机的三级调度

三级调度

  • 作业调度(高级调度)
  • 内存调度(中级调度)
  • 进程调度(低级调度)

作业调度从外存的后备队列中选择一批作业进入内存,为它们建立进程,这些进程被送入就绪队列,进程调度从就绪队列中选出一个进程,并把其状态改为运行状态,把CPU分配给它。中级调度是为了提高内存的利用率,系统将那些暂时不能运行的进程挂起来。当内存空间宽松时,通过中级调度选择具备运行条件的进程,将其唤醒。

  • 作业调度为进程活动做准备,进程调度使进程正常活动起来,中级调度将暂时不能运行的进程挂起,中级调度处于作业调度和进程调度之间。
  • 作业调度次数少,中级调度次数略多,进程调度频率最高。
  • 进程调度是最基本的,不可或缺。

推荐一篇文章# Linux进程

[TOC]

进程是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。
线程:单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位。
换句话说,进程线程都是对CPU工作时间段的描述(通过task_struct)。

而在Linux系统运行中,程序对应的进程是怎么被创建,被管理,被切换,被调度的呢?

进程的组成,创建,状态机

进程的组成,创建

所有进程都会有一个共同的父进程,PID为1的init(systemd)进程,它是内核引导后启动的第一个进程,主要用来执行一些开机初始化脚本和监视进程,读取配置文件/etc/inittab,永远不会停止。必要的时候可以作为参照。如果某个进程突然成为游离进程(其父进程终止了),此时,这个进程会将init进程作为参照,作为它们的父进程,然后gg。父进程死了它的子进程也会死。可以通过pstree查看系统内正在运行的各个进程的之间的继承关系。
inittab 是一个不可执行的文本文件,每行的基本格式为:id:runlevels:action:process

  • id: 1-2个字符,配置行的唯一标识,不能重复。
  • runlevels: 配置行适用的运行级别,可以填入多个运行级别,如12345或者35。
    • 0: 关机
    • 1: 单用户字符界面
    • 2: 不具备网络文件系统(NFS)功能的多用户文字界面
    • 3: 具备网络功能的多用户字符界面
    • 4: 保留
    • 5: 具有网络功能的图形用户界面
    • 6: 重新启动系统
  • action: 表示init的行为,行为表略。
  • process: 为init执行的进程,这些进程都保存在/etc/rcX.d/中,其中X代表运行级别,rc程序接受参数X,然后执行/etc/rcX.d/下面的程序。
    • 这些程序其实都是符号链接,以S打头的表示启动该程序,而以K打头的标识终止该程序,后面的数字标识执行顺序,越小越先执行,剩下的标识程序名。
    • 系统启动或者切换到该运行级别时会执行以S打头的程序,系统切换到其他运行级别是会终止以K打头的程序。
    • 可以通过chkconfig程序进行管理
      init程序也是一个进程,和普通进程拥有一样的属性,比如修改了/etc/inittab,想要立即生效,可以通过运行kill -SIGHUP 1来实现,也可以通过init q来实现。

kill命令的工作原理是:向Linux系统的内核发送一个系统操作信号和某个程序的进程标识号,然后系统内核就可以对进程标识号指定的进程进行操作。

在Linux内核中,进程通过一个双向链表来进行管理,每一个进程包含一个进程描述符(task_struct)

  • 进程描述符
    • ​当把一个程序加载到内存当中,此时,这个时候就有了进程,关于进程,有一个相关的叫做进程控制块(PCB),这个是系统为了方便进行管理进程所设置的一个数据结构,通过PCB,就可以记录进程的特征以及一些信息。 内核当中使用进程描述符task_struct。 这个task_struct就是一个定义的一个结构体,通过这个结构体,可以对进程的所有的相关的信息进行维护,对进程进行管理。
    • 推荐一篇文章
  • 进程的组成
    • 程序读取的上下文,表示程序读取执行的状态
    • 程序当前执行的目录
    • 程序服务的文件与目录
    • 程序访问的权限
    • 内存和其他分配给进程的系统资源
  • 进程的创建
    • 进程总是通过fork系列的系统调用来创建的,最终都是调用do_fork函数,只是参数上的不同而已

      • fork(do_fork(CLONE_SIGCHLD, …))

        • fork创建一个进程时,子进程只是完全复制父进程的资源,复制出来的子进程有自己的task_struct结构和pid,但却复制父进程其它资源(用户空间、文件描述符集)。
        • 采用cow(copy on write)写时复制来降低系统调用的开销。
        • 一次返回两个值,对于父进程,是子程序的pid,而对于子程序,成功状态下是0。
        • 在fork之后,子进程会获得父进程的数据段,堆栈段的副本,而代码段则是共享的。
      • clone(do_fork(CLONE_VM|CLONE_FS|CLONE_FILES|CLONE_SIGCHLD, …))

        • fork()是全部复制,vfork()是共享内存,而clone()是则可以将父进程资源有选择地复制给子进程,前两者都是不带参数的,后者有参数clone_flags。
      • vfork(do_fork(CLONE_VFORK|CLONE_VM|CLONE_SIGCHLD, …))

        • vfork系统调用不同于fork,用vfork创建的子进程与父进程共享地址空间,也就是说子进程完全运行在父进程的地址空间上,如果这时子进程修改了某个变量,这将影响到父进程。

        • vfork也是在父进程中返回子进程的进程号,在子进程中返回0。

        • 用 vfork创建子进程后,父进程会被阻塞直到子进程调用exec(exec,将一个新的可执行文件载入到地址空间并执行之)或exit。

        • vfork的好处是在子进程被创建后往往仅仅是为了调用exec执行另一个程序,因为它就不会对父进程的地址空间有任何引用,所以对地址空间的复制是多余的 ,因此通过vfork共享内存可以减少不必要的开销。

      • 三者的区别

        • 拷贝内容
        • 访问次序控制
          • fork不对父子进程的执行次序进行任何限制,运行顺序由内核的调度算法决定。
          • vfork调用后,子进程先运行,父进程挂起,直到子进程调用了exec或者exit后,父子进程的执行次序才不再有限制。
          • clone中有标志CLONE_VFORK来决定子进程在运行是父进程是阻塞的还是运行的,如果没有设置,则同时进行,设置了,与fork一样。
  • CLONE_VM标识:表示共享地址空间(变量等)

    CLONE_FILES标志:表示共享文件描述符表

    CLONE_VFORK标识:标识父进程会被阻塞,子进程会把父进程的地址空间锁住,直到子进程退出或执行exec时才释放该锁

    SIGCHLD标识:共享信号

  • 内核也可以通过kernel_thread函数来创建内核进程

Linux 进程状态机

进程状态转换关系图

  • 虽然进程的状态有好多种,但是进程状态的变迁只有两个方向,从TASK_RUNNING到非TASK_RUNNING,或者相反。总之,进程状态的转换必然会经过TASK_RUNNING,不可能在两个非RUN状态直接转换。
  • 进程从非TASK_RUNNING状态变为TASK_RUNNING状态,是由别的进程(也可能是中断处理程序)执行唤醒操作来实现的。执行唤醒的进程设置被唤醒进程的状态为TASK_RUNNING,然后将其task_struct结构加入到某个CPU的可执行队列中。于是被唤醒的进程将有机会被调度执行。
  • 进程从TASK_RUNNING状态变为非TASK_RUNNING状态,有两种途径。
    • 响应信号而进入TASK_STOPED状态,或者TASK_DEAD状态
    • 执行程序主动进入TASK_INTERRUPTIBLE状态(如nanosleep调用),或者是TASK_DEAD状态(如exit系统调用),另一方面,也有可能是由于执行系统调用需要的资源得不到满足,而进入TASK_INTERRUPTIBLE状态或TASK_UNINTERRUPTIBLE状态(如select系统调用)。
  • 可以通过ps命令来查看系统中存在的进程,以及它们的状态。
  • 系统中绝大部分进程都处于TASK_INTERRUPTIBLE状态,因为CPU也就那么几个,所以绝大部分进程都处于睡眠状态。
  • TASK_UNINTERRUPTIBLE,不可中断的睡眠状态,指该进程不接受异步信号,如kill -9无法杀死该类进程,这种状态的意义在于,内核中的某些处理流程是不能被打断的,如果响应了异步信号,程序的执行流程中就会给插入了一段处理异步信号的流程(可能只存在于内核态,也可能会延伸到用户态)于是乎,原有的流程就给中断了。 而且,一般系统进程中存在于这个状态的进程是非常少,而且正常来说,都是非常短暂的,基本无法察觉得到的。其实通过vfork调用后,父进程就是处于一种TASK_UNINTERRUPTIBLE的状态,知道子进程调用exec或者exit。
  • TASK_STOPPED 或者 TASK_TRACED,暂停状态或跟踪状态。
    • 向某个进程发送SIGSTOP信号或者SIGCONT信号可以进入TASK_STOPPED或者恢复到TASK_RUNNING。
    • TASK_TRACED状态一般只发生在该进程被调试暂停的时候,该进程停下来,等待跟踪它的进程对其进行操作,TASK_TRACED状态下的进程不响应SIGCONT信号,只能是调试它的进程来恢复它,或者调试进程退出时也会恢复。
  • TASK_DEAD - EXIT_ZOMBIE,退出状态,进程成为僵尸进程。
    • 进程在退出的过程中,处于TASK_DEAD状态,在退出过程中,进程所占有的资源将会被回收,除了task_struct结构(以及少数资源),也就是说,进程表只存在这个进程对应的task_struct这个空壳,所以叫做僵尸,由父进程负责收集,收尸。从这个尸体(task_struct)中可以得到很多有用的信息,如子进程的退出码,一些统计信息(虽然内核也可以吧这些信息保存在其他的地方,但是不这么做)。父进程可以通过wait系列的系统调用来等待子进程的退出,然后获取退出信息,然后wait系列的系统调用会清理尸体(task_struct)。
    • 子进程在退出的时候,内核会给父进程发送一个信号(SIGCHLD)来通知父进程过来收尸。
    • 进程可以通过调用exit来进入该状态。
  • TASK_DEAD - EXIT_DEAD,退出状态,进程即将给销毁。
    • 进程也有可能不会经历TASK_DEAD - EXIT_ZOMBIE状态而直接销毁。如detach过的进程,SIGCHLD信号的handle为SIG_IGN,忽略了SIGCHLD信号。

进程的调度

无论是在批处理系统还是分时系统中,用户进程数一般都多于处理机数、这将导致它们互相争夺处理机。另外,系统进程也同样需要使用处理机。这就要求进程调度程序按一定的策略,动态地把处理机分配给处于就绪队列中的某一个进程,以使之执行。这就是进程调度。

当cpu中正在执行的进程(process A)被移出(suspend),cpu中当前进程的相关信息写到需要移除的进程的进程描述符(task_strut)中,下一个进程(process B)被内核读取(该过程称为resume,恢复)到cpu中, 注意:进程的切换也是必须由内核来执行的。进程的切换就是通过用户空间和内核模式的不断切换来完成的。进程切换=CPU加载上下文+CPU执行+CPU保存上下文

Linux 进程调度的目标

  • 高效性: 高效性意味着在相同时间下要完成更多的任务。调度程序会被频繁的执行,所以调度程序要尽可能的高效。
  • 加强交互性能: 在系统相当的负载下,也要保证系统的响应时间。
  • 保证公平和避免饥渴
  • SMP调度: 调度程序必须支持多处理系统。
  • 软实时调度: 系统必须有效的调用实时进程,但不保证一定满足其要求。

Linux 进程优先级

  1. 普通进程的优先级
    • 使用SCHED_NORMAL调度策略
  2. 实时进程的优先级
    • 使用SCHED_FIFO(一次机会做完) 或者 SCHED_RR(多次轮转) 调度策略

任何时候,实时进程的优先级都高于普通进程,实时进程只会被更高级的实时进程抢占

Linux 进程调度

  • 实时进程调度
    • 实时进程,只有静态优先级,因为内核不会根据休眠等因素对其静态优先级做调整,范围是在0 - MAX_RT_PRIO - 1 之间。而nice值,影响的优先级在MAX_RT_PRIO - MAX_RT_PRIO + 40之间的进程。

    • MAX_RT_PRIO: 默认配置为100.

    • 不同于普通进程,实时进程高的进程总是先于优先级低的进程执行,直到实时优先级高的实时进程无法执行,实时进程总是被认为处于活动状态。

    • 如果有数个进程相同的实时进程,那么系统会按照进程在队列上的顺序选择进程。

    • 例如CPU运行实时进程A的实时优先级为a,而此时有一个实时优先级为b的实时进程B进入可运行状态,那么只要b > a,系统将会中断A的执行,而优先执行B,直到B无法执行。

    • 调度策略
      • FIFO 的进程,意味着只有当前进程执行完毕才会轮到其他进程执行。
      • RR 的进程,内核会为进程分配时间片,一旦时间片消耗完毕,内核会将该进程置于进程队列的末尾,然后运行其他相同优先级别的进程,如果没有其他相同优先级的进程,则该进程会继续执行。

对于实时进程,高优先级别的进程执行到无法执行了,才会轮到低优先级别的进程执行。

  • 非实时进程调度
    • Linux 对于普通进程,根据动态优先级进行调整,而动态优先级是由静态优先级(static_prio)调整而来的。 静态优先级是用户不可见的,隐藏在内核中,而内核会给用户一个可以影响静态优先级的接口,那就是nice值。

    • static_prio = MAX_RT_PRIO + nice + 20

    • nice 值与静态优先级的关系如上所示。nice值的范围是-20 - 19之间,所以静态优先级范围在100 - 139之间。

    • 普通进程的动态优先级越低,优先越高,与实时进程相反,也就是说,nice的值越大,优先级越低。

进程时间片是完全依赖于static_prio 定制的,如下所示

$$
base\ time\ quantum(ms) =
\begin{cases}
(140 - \text{static_prio})\times20, \text{static_prio < 120}\
(140 - \text{static_prio})\times20, \text{static_prio } \ge \text{ 120}
\end{cases}
$$

进程调度的其他因素

系统在调度进程的时候,还会考虑其他因素,进而来计算出一个叫进程动态优先级的东西,基于这个来实施调度。

  • 进程属性
    • 交互式进程(I/O密集型,等待IO,对cpu的需求量小)
    • 批处理进程(CPU密集型)
    • 实时进程
  • 分配策略
    • 不仅要考虑静态优先级,也要考虑进程的属性,如果进程是属于交互式进程,那么可以适当的调高它的优先级,使得界面反应更加迅速,从而提高用户体验。

    • 交互式进程可以从一个平均睡眠时间来进行判断,如果进程过去睡眠的时间越多,则越有可能属于交互式进程。则系统在调度的时候,会给该进程更多的奖励(bonus),以便该进程有更多的时间能够执行。奖励(bonus)的范围为0 - 10。

    • 睡眠和CPU耗时反应了进程IO密集和CPU密集两大瞬时特点,不同时期,一个进程可能即是CPU密集型也是IO密集型进程。对于表现为IO密集的进程,应该经常运行,但每次时间片不要太长。对于表现为CPU密集的进程,CPU不应该让其经常运行,但每次运行时间片要长。交互进程为例,假如之前其其大部分时间在于等待CPU,这时为了调高相应速度,就需要增加奖励分。另一方面,如果此进程总是耗尽每次分配给它的时间片,为了对其他进程公平,就要增加这个进程的惩罚分数。

    • 系统会严格按照动态优先级高低的顺序安排进程执行。动态优先级高的进程进入运行状态,或者时间片消耗完毕后才会轮到动态优先级低的进程执行。

    • 动态优先级的计算主要考虑两个因素

      • 静态优先级(static_prio)
      • 平均睡眠时间(bonus)
    • 计算公式如下
      $$
      \text{dynamic_prio} = max{100,\ min{static_prio- bonus+5,\ 139} }
      $$

    • Linux2.6在计算的时候有空间换时间的策略,从而保证最优进程能够在O(1)的时间内完成。

    • CFS 的virtutime机制。

进程切换 (Context switch)

当cpu中正在执行的进程(process A)被移出(suspend),cpu中当前进程的相关信息写到需要移除的进程的进程描述符(task_strut)中,下一个进程(process B)被内核读取(该过程称为resume,恢复)到cpu中, 注意:进程的切换也是必须由内核来执行的。进程的切换就是通过用户空间和内核模式的不断切换来完成的。进程切换=CPU加载上下文+CPU执行+CPU保存上下文

调度触发的时机

  1. 当前进程进入非可执行状态

  2. 抢占

    • 进程用完了时间片
    • 出现优先级更高的进程,会被影响而唤醒。

    内核在响应时钟中断的过程中,发现当前进程的时间片已经用完

    内核在响应时钟中断的过程中,发现优先级更高的进程所等待的外部资源是可用的,于是唤醒它。

其他相关

内核抢占

  • 理想情况下,只要满足了出现优先级更高的进程这个条件,当前进程就应该被立刻抢占。但是就像多线程程序需要使用锁来保护临界区资源一样,内核中也有很多这样的临界区,来保证一些程序不给抢占。或者是处于效率考虑。
  • Linux2.4不支持内核抢占,进程在内核态是不允许抢占的,只有返回用户态时会触发调整。
  • Linux2.6支持内核抢占,但是很多地方为了保护临界区资源而临时性的禁用抢占。

SMP调度下的负载均衡问题

多处理器下,每个CPU都有对应的可执行队列,但是一个可执行状态的进程同一时刻只能存在于一个可执行队列中。

问题

  • 每个CPU可执行队列中进程数目不均衡的时候,要怎么调整,什么时候算作是不均衡,需要调整?
  • CPU之间的关系,两个CPU之间,是不是相互独立的,cache可以共享吗?或者,是不是由同一个物理CPU通过超线程技术虚拟出来的
  • 反正就是乱七八糟的就是了。。。。。

优先级继承

假如有一个进程A,因为要进入临界区,但是临界区有了B,而在等待睡眠,直到B退出临界区后,进程A才会被唤醒。

可能存在这么一种情况,A的优先级很高,而B的优先级很低,B进入了临界区,但是却给其他优先级高于B低于A的进程C抢占,A就等不到运行,此时叫做优先级反转。此时会通过以下方法解决:在A开始等待的时候,将A的优先级赋给B,直到B退出临界区后恢复其原本的优先级。

这就是优先级继承。

中断处理程序的优化

正常来说,中断处理程序处于一个不可调度的上下文中,CPU响应硬件中断自动跳转到内核设定的中断处理程序去执行,再到处理程序执行完。这整个过程中是不能被抢占的。

一个进程如果被抢占了,可以通过保存在它的进程控制块(task_struct)中的信息,在之后的某个时间恢复它的运行,而中断上下文是没有task_struct的,所以是不可能杯抢占的。

也就是说,中断程序的伪优先级比任何进程要高,但是呢在实际中,某些实时进程会拥有比中断进程更高的优先级。所以在一些系统中,会给中断处理程序赋予task_struct以及优先级。但是会影响一些性能,但是却能保证实时。

调度程序的效率问题

优先级明确了哪个进程应该被调度执行,而调度程序还要关心效率问题,因为会被频繁的执行,所以效率很重要的。

所以,调度程序,如何来获得应该被执行的程序,就是一个问题了。

在Linux2.4,可执行状态的进程会被放在一个链表中,每次调度,扫描整个链表,以找出最优的那个进程来运行,复杂度为O(N)

在Linux2.6早期,可执行状态的进车功能会被挂载N(N=140)个链表中,每一个链表代表一个优先级,系统支持多少个优先级就有多少个链表。每次调度,调度程序只需要从第一个不为空的链表中取出链表头的进程即可。复杂度为O(1).

而在现在Linux2.6的版本中,可执行状态的进程会按照优先级顺序被挂载一个红黑树上,每次调度,找出树中优先级最高的那个进程。复杂度为O(LogN)

只是为了公平

第一种是基于空间换时间的策略,在一组数目不大的链表中来实现的,这样子优先级的取值范围很小(大了会浪费太多空间),区分度不大不能满足公平性的需求,而使用红黑树对优先级的取值没有限制,区分度可以更高。而且在同等情况下,140个优先级,O(LogN)的复杂度也不会太差。

处理机的三级调度

三级调度

  • 作业调度(高级调度)
  • 内存调度(中级调度)
  • 进程调度(低级调度)

作业调度从外存的后备队列中选择一批作业进入内存,为它们建立进程,这些进程被送入就绪队列,进程调度从就绪队列中选出一个进程,并把其状态改为运行状态,把CPU分配给它。中级调度是为了提高内存的利用率,系统将那些暂时不能运行的进程挂起来。当内存空间宽松时,通过中级调度选择具备运行条件的进程,将其唤醒。

  • 作业调度为进程活动做准备,进程调度使进程正常活动起来,中级调度将暂时不能运行的进程挂起,中级调度处于作业调度和进程调度之间。
  • 作业调度次数少,中级调度次数略多,进程调度频率最高。
  • 进程调度是最基本的,不可或缺。

推荐一篇文章

原文作者:dcLunatic

原文链接:http://dclunatic.github.io/Linux%E8%BF%9B%E7%A8%8B%E7%AE%A1%E7%90%86.html

发表日期:September 30th 2018, 9:52:51 am

更新日期:July 11th 2021, 9:13:49 pm

版权声明:转载的时候,记得注明来处

CATALOG
  1. 1. Linux进程
    1. 1.1. 而在Linux系统运行中,程序对应的进程是怎么被创建,被管理,被切换,被调度的呢?
      1. 1.1.1. 进程的组成,创建,状态机
        1. 1.1.1.1. 进程的组成,创建
          1. 1.1.1.1.1. 进程描述符
          2. 1.1.1.1.2. 进程的组成
          3. 1.1.1.1.3. 进程的创建
        2. 1.1.1.2. Linux 进程状态机
      2. 1.1.2. 进程的调度
        1. 1.1.2.1. Linux 进程调度的目标
        2. 1.1.2.2. Linux 进程优先级
          1. 1.1.2.2.1. 普通进程的优先级
          2. 1.1.2.2.2. 实时进程的优先级
        3. 1.1.2.3. Linux 进程调度
          1. 1.1.2.3.1. 实时进程调度
            1. 1.1.2.3.1.1. 调度策略
          2. 1.1.2.3.2. 非实时进程调度
        4. 1.1.2.4. 进程调度的其他因素
          1. 1.1.2.4.1. 进程属性
          2. 1.1.2.4.2. 分配策略
        5. 1.1.2.5. 进程切换 (Context switch)
        6. 1.1.2.6. 调度触发的时机
      3. 1.1.3. 其他相关
        1. 1.1.3.1. 内核抢占
        2. 1.1.3.2. SMP调度下的负载均衡问题
        3. 1.1.3.3. 优先级继承
        4. 1.1.3.4. 中断处理程序的优化
        5. 1.1.3.5. 调度程序的效率问题
        6. 1.1.3.6. 处理机的三级调度
    2. 1.2. 而在Linux系统运行中,程序对应的进程是怎么被创建,被管理,被切换,被调度的呢?
      1. 1.2.1. 进程的组成,创建,状态机
        1. 1.2.1.1. 进程的组成,创建
          1. 1.2.1.1.1. 进程描述符
          2. 1.2.1.1.2. 进程的组成
          3. 1.2.1.1.3. 进程的创建
        2. 1.2.1.2. Linux 进程状态机
      2. 1.2.2. 进程的调度
        1. 1.2.2.1. Linux 进程调度的目标
        2. 1.2.2.2. Linux 进程优先级
          1. 1.2.2.2.1. 普通进程的优先级
          2. 1.2.2.2.2. 实时进程的优先级
        3. 1.2.2.3. Linux 进程调度
          1. 1.2.2.3.1. 实时进程调度
            1. 1.2.2.3.1.1. 调度策略
          2. 1.2.2.3.2. 非实时进程调度
        4. 1.2.2.4. 进程调度的其他因素
          1. 1.2.2.4.1. 进程属性
          2. 1.2.2.4.2. 分配策略
        5. 1.2.2.5. 进程切换 (Context switch)
        6. 1.2.2.6. 调度触发的时机
      3. 1.2.3. 其他相关
        1. 1.2.3.1. 内核抢占
        2. 1.2.3.2. SMP调度下的负载均衡问题
        3. 1.2.3.3. 优先级继承
        4. 1.2.3.4. 中断处理程序的优化
        5. 1.2.3.5. 调度程序的效率问题
        6. 1.2.3.6. 处理机的三级调度