Linux 进程状态、孤儿进程和僵尸进程

Linux 进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。

进程执行时的间断性,决定了进程可能具有多种状态。

运行中的进程可能具有三种基本状态:

1 就绪状态(ready):进程已经获取了除CPU意外的所需资源,等待分配CPU资源;只要分配了CPU进程就可以执行。就绪进程可以按多个优先级来划分队列。

例如,当一个进程由于时间片用完而进入就绪状态时,排入低优先级队列;当进程由I/O操作完成而进入就绪状态时,排入高优先级队列。

2 运行状态(Running)

进程占用CPU资源;处于此状态的进程的数目小于等于处理器的数目。在没有其他进程可以执行时(如所有进程都在阻塞状态),通常会自动执行系统的空闲进程。

3 阻塞状态(Blocked)

由于进程等待某种条件(如I/O操作或进程同步)或者某事件发生,在条件满足之前无法继续执行。该事件发生前即使把处理器资源分配给该进程,也无法运行

(tips:要理解网络IO的几种状态要先理解进程状态。)

    总之一句话 :运行占用CPU,就绪只差CPU,阻塞在等一件事。

进程还有  就绪挂起(ready suspended),阻塞挂起(blocked suspended),

     就绪/挂起:把就绪态的进程从内存中换出到虚拟内存中。进程在VM(虚拟内存)中,只要调入主存(物理内存)中就能运行。

     阻塞/挂起:把阻塞态的进程从内存中换出到虚拟内存中。当内存中所有进程都阻塞的时候,CPU就把其中一个阻塞的进程调到VM中。这个就是阻塞挂起。

五个问题帮助理解:

1 阻塞挂起的状态和阻塞状态 最大的不同点是什么?

        阻塞状态的进程在主内存中,阻塞挂起状态的进程在虚拟内存中。

2 阻塞挂起和就绪挂起 最大的不同点是什么?

     阻塞挂起 等待的事件发生之后便进入就绪挂起。最大的不同是等待的事件有没有发生。

3 什么情况下会使进程 由就绪转到就绪/挂起状态?

      多个就绪进程优先级比较低,有高优先级的阻塞(操作系统OS 认为很快就会就绪)进程和低优先级就绪进程时,系统会选择挂起低优先级就绪进程。

4 什么情况下会使进程 从阻塞/挂起变成阻塞状态

      当一个进程释放足够的内存时,系统会把一个高优先级的阻塞/挂起进程 调入内存。

5 什么情况下会使进程 从运行状态变成就绪挂起状态?

          对抢占式系统,当有高优先级阻塞挂起进程因事件出现而进入就绪挂起时,系统可能会把运行进程(优先级比较低)转到就绪挂起状态

         通常当分配给一个运行进程的时间片用完时,它将转换到就绪态。但是,如果由于位于阻塞/挂起队列中具有较高优先级的进程变得不再被阻塞,操作系统抢占这个进程,也可以直接把这个运行进程转换到就绪/挂起队列中,并释放一些内存空间。

   

尝试着画了一张图 来帮助自己记忆。

image.png

在Linux中man ps 可以看到的以下进程的状态的说明:

S 进程状态 Process Status .包括以下几种状态: 

D = uninterruptible sleep 不可中断 

R = running or runnable 运行或者就绪的

S = sleeping 睡眠 

T = traced or stopped 追踪或者停止 

Z = zombie 僵尸 进程结束,没有被其父进程回收。

X = dead 一般看不到

<    high-priority (高优先级)

N    low-priority (低优先级)

L   页面是否已锁定到内存中

s    代表这个进程是一个会话的领导进程

l    多线程(using CLONE_THREAD, like NPTL pthreads do)

+   代表前台进程组

进程组:进程组表示一组相互关联的进程,比如每个子进程都是父进程所在组的成员

会话:而会话是指共享同一个控制终端的一个或者多个进程组

比如,我们通过 SSH 登录服务器,就会打开一个控制终端(TTY),这个控制终端就对应一个会话。而我们在终端中运行的命令以及它们的子进程,就构成了一个个的进程组,其中,在后台运行的命令,构成后台进程组;在前台运行的命令,构成前台进程组。

二  orphan process  孤儿进程

     在类UNIX(unix-like)操作系统中,子进程是通过父进程创建的,子进程再创建新的进程。

子进程的结束和父进程的运行是一个异步过程,即父进程永远无法预测子进程 到底什么时候结束。

当一个父进程由于正常完成工作而退出或由于其他情况被终止,它的一个或多个子进程却还在运行,那么那些子进程将成为孤儿进程。

为避免孤儿进程退出时无法释放所占用的资源而僵死,进程号为1的进程将会接受这些孤儿进程,这一过程也被称为“收养”(英语:re-parenting)

因为有1号大哥进程在“善后”,孤儿进程并不会有什么危害。

三  zombie process 僵尸进程

正常情况下,当一个进程创建了子进程后,它应该通过系统调用 wait() 或者 waitpid() 等待子进程结束,回收子进程的资源;而子进程在结束时,会向它的父进程发送 SIGCHLD 信号,所以,父进程还可以注册 SIGCHLD 信号的处理函数,异步回收资源。

僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程。释放子进程占用的资源,此时子进程就变成了僵尸进程。

任何一个子进程(init除外)在exit()之后,并非马上就消失掉,而是留下一个称为僵尸进程(Zombie)的数据结构,等待父进程处理

僵尸进程的危害:

在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。

但是仍然为其保留一定的信息(包括进程号the process ID,退出状态the termination status of the process,运行时间the amount of CPU time taken by the process等)。

直到父进程通过wait / waitpid来取时才释放. 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵死进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。

僵尸进程的清理方案:

1 杀死僵尸进程的父进程

僵尸进程避免方案:

1 两次fork:而且使紧跟的子进程直接退出,是的孙子进程成为孤儿进程,从而init进程将负责清除这个孤儿进程。

2 主动忽略子进程的信号通知。通过 signal(SIGCHLD,SIG_IGN) 通知内核对子进程结束不关心,由内核回收。SIGCHLD 是子进程退出的时候像父进程发送的。SIG_IGN 表示信号处理方式为忽略。

这种方法常用于并发服务器性能提升。因为并发服务器常常fork很多子进程,子进程终结之后需要服务器进程去wait清理资源。如果将此信号的处理方式设为忽略,可让内核把僵尸子进程转交给init进程去处理,省去了大量僵尸进程占用系统资源。

3 父进程调用wait/waitpid等函数等待子进程结束,如果尚无子进程退出wait会导致父进程阻塞。waitpid可以通过传递WNOHANG使父进程不阻塞立即返回。

4 如果父进程很忙可以用signal注册信号处理函数,在信号处理函数调用wait/waitpid等待子进程退出。异步回收资源。

总结:孤儿进程 是指子进程还在运行,而父进程提前退出,导致子进程被1号进程善后.孤儿进程没什么危害。

  僵尸进程是指,父进程在子进程运行完毕后,没有回收其资源,导致僵尸进程一直存在,浪费进程号,浪费内存。

参考链接:https://www.cnblogs.com/wuchanming/p/4020463.html

                 https://unordered.org/timelines/59cd509019c01000

                http://jjplane.com/index.php/2017/10/13/%E6%93%8D%E4%BD%9C%E7%B3%BB%E7%BB%9F-%E9%9D%99%E6%AD%A2%E9%98%BB%E5%A1%9E%E6%B4%BB%E5%8A%A8%E9%98%BB%E5%A1%9E%E9%9D%99%E6%AD%A2%E5%B0%B1%E7%BB%AA%E6%B4%BB%E5%8A%A8%E5%B0%B1%E7%BB%AA/

Linux 内存是怎么工作的?

内存管理是操作系统最核心的功能之一。内存主要用来存储系统和应用程序的指令,数据,缓存等。

内存映射

   比如说我现在电脑的内存是8GB,这个内存容量是8GB,指的是我的内存条的容量的大小。指的是物理内存。物理内存也称为主存,大多数计算机用的主存都是动态随机访问内存(DRAM dynamic random access memory)。只有内核才可以直接访问物理内存。那么,进程要访问内存时,该怎么办呢?

   

   linux 内核给每个进程都提供了一个独立的虚拟地址空间。并且这个地址空间是连续的。这样,进程就可以很方便的访问内存了,更确切的说是访问虚拟内存。

   

   虚拟地址空间的内部又被分为**内核空间**和**用户空间**两部分,不同字长(也就是单个 CPU 指令可以处理数据的最大长度)的处理器,地址空间的范围也不同。比如最常见的 32 位和 64 位系统,我画了两张图来分别表示它们的虚拟地址空间,如下所示:

   image.png

通过这里可以看出,32 位系统的内核空间占用 1G,位于最高处,剩下的 3G 是用户空间。而 64 位系统的内核空间和用户空间都是 128T,分别占据整个内存空间的最高和最低处,剩下的中间部分是未定义的。

还记得进程的用户态和内核态吗?进程在用户态时,只能访问用户空间内存;只有进入内核态后,才可以访问内核空间内存。虽然每个进程的地址空间都包含了内核空间,但这些内核空间,其实关联的都是相同的物理内存。这样,进程切换到内核态后,就可以很方便地访问内核空间内存。

内核态与用户态

内核态:当进程运行在内核空间时就处于内核态

用户态:而进程运行在用户空间时就处于用户态

在内核态下,进程运行在内核地址空间中,此时 CPU 可以执行任何指令。运行的代码也不受任何的限制,可以自由地访问任何有效地址,也可以直接进行端口的访问。

在用户态下,进程运行在用户地址空间中,被执行的代码要受到 CPU 的诸多检查,它们只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址,且只能对任务状态段(TSS)中 I/O 许可位图(I/O Permission Bitmap)中规定的可访问端口进行直接访问。

      既然每个进程都有一个这么大的地址空间,那么所有进程的虚拟内存加起来,自然要比实际的物理内存大得多。所以,并不是所有的虚拟内存都会分配物理内存,只有那些实际使用的虚拟内存才分配物理内存,并且分配后的物理内存,是通过内存映射来管理的。

内存映射,其实就是将虚拟内存地址映射到物理内存地址。为了完成内存映射,内核为每个进程都维护了一张页表,记录虚拟地址与物理地址的映射关系,如下图所示:

image.png

页表实际上存储在 CPU 的内存管理单元 MMU(Memory Management Unit) 中,这样,正常情况下,处理器就可以直接通过硬件,找出要访问的内存。

而当进程访问的虚拟地址在页表中查不到时,系统会产生一个缺页异常,进入内核分配物理内存,更新进程页表,最后再返回用户空间,恢复系统的运行。

缺页异常:有些地方翻译成缺页中断,缺页异常才更合适,英文叫做“Page Fault

指的是当软件试图访问已映射在虚拟地址空间中,但是当前并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断。—维基百科

什么是page fault?

当进程访问它的虚拟地址空间中的PAGE时,如果这个PAGE目前还不在物理内存中,此时CPU是不能干活的, 

Linux会产生一个hard page fault中断。 

系统需要从慢速设备(如磁盘)将对应的数据PAGE读入物理内存,并建立物理内存地址与虚拟地址空间PAGE的映射关系。 

然后进程才能访问这部分虚拟地址空间的内存。

page fault 又分为几种,major page fault、 minor page fault、 invalid(segment fault)。

major page fault也称为hard page fault, 指需要访问的内存不在虚拟地址空间,也不在物理内存中,需要从慢速设备载入。从swap回到物理内存也是hard page fault。

minor page fault也称为soft page fault, 指需要访问的内存不在虚拟地址空间,但是在物理内存中,只需要MMU建立物理内存和虚拟地址空间的映射关系即可。 

(通常是多个进程访问同一个共享内存中的数据,可能某些进程还没有建立起映射关系,所以访问时会出现soft page fault)

invalid fault也称为segment fault, 指进程需要访问的内存地址不在它的虚拟地址空间范围内,属于越界访问,内核会报segment fault错误。

TLB(Translation Lookaside Buffer 转译后备缓冲区) 页表缓存。是CPU的一种缓存,由MMU用于改进虚拟地址到物理地址的转译速度。

TLB具有固定数目的空间槽,用于存放将虚拟地址映射至物理地址的标签页表条目。其搜索关键字为虚拟内存地址,其搜索结果为物理地址。如果请求的虚拟地址在TLB中存在,CAM (content-addressable memory 相联存储器)将给出一个非常快速的匹配结果,之后就可以使用得到的物理地址访问存储器。如果请求的虚拟地址不在 TLB 中,就会使用标签页表进行虚实地址转换,而标签页表的访问速度比TLB慢很多。

TLB 其实就是 MMU 中页表的高速缓存。由于进程的虚拟地址空间是独立的,而 TLB 的访问速度又比 MMU 快得多,所以,通过减少进程的上下文切换,减少 TLB 的刷新次数,就可以提高 TLB 缓存的使用率,进而提高 CPU 的内存访问性能。

MMU的规定了一个内存映射的最小单位,也就是页,通常是4KB 大小。这样每次内存映射都需要关联4Kb或者4KB整数倍的内存空间。

(物理内存中对应的单位称为页帧) 他们俩大小总是一样的

 

页的大小只有 4 KB ,导致的另一个问题就是,整个页表会变得非常大。比方说,仅 32 位系统就需要 100 多万个页表项(4GB内存/4KB页大小),才可以实现整个地址空间的映射。为了解决页表项过多的问题,Linux 提供了两种机制,也就是多级页表和大页(HugePage)。(可以尝试理解 页表就像mysql的表,而页就是其中一行)

多级页表

多级页表就是把内存分成区块来管理,将原来的映射关系改成区块索引和区块内的偏移(offset)。由于虚拟内存空间通常只用了很少一部分,那么,多级页表就只保存这些使用中的区块,这样就可以大大地减少页表的项数。

Linux 用的正是四级页表来管理内存页,如下图所示,虚拟地址被分为 5 个部分,前 4 个表项用于选择页,而最后一个索引表示页内偏移。

image.png

大页(Huge page):

在虚拟内存管理中,内核维护一个将虚拟内存地址映射到物理地址的表,对于每个页面操作,内核都需要加载相关的映射。

如果你的内存页很小,那么你需要加载的页就会很多,导致内核会加载更多的映射表。而这会降低性能。

使用“大内存页”,意味着所需要的页变少了。从而大大减少由内核加载的映射表的数量。并且所需的地址转换也减少了,TLB缓存失效的次数就减少了。这提高了内核级别的性能最终有利于应用程序的性能。

简而言之,通过启用“大内存页”,系统具只需要处理较少的页面映射表,从而减少访问/维护它们的开销!

另外,由于地址转换所需的信息一般保存在CPU的缓存中,huge page的使用让地址转换信息减少,从而减少了CPU缓存的使用,减轻了CPU缓存的压力,让CPU缓存能更多地用于应用程序的数据缓存,也能够在整体上提升系统的性能

假设 需要 32Mb 内存  标准页为4kb 假设一个页表只能装10个页,那么需要 32*1024/4 =8192个页 大概需要820个页表。如果提升标准页大小 比如1M,32/1=32/10=3.2 大约需要4个页表就能满足了

二、虚拟内存空间分布

    image.png

           32位系统

通过这张图你可以看到,用户空间内存,从低到高分别是五种不同的内存段。

1 只读段,包括代码和常量等。也可以叫做代码段

2 数据段,包括全局变量,静态变量的空间等

3 堆,包括动态分配的内存,从低地址开始向上增长。

4 文件映射段,包括动态库、共享内存等,从高地址开始向下增长。一般是mmap函数所分配的虚拟地址空间

5 栈 包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是8MB  查看方式ulimit -s

内核空间:

内核总是驻留在内存中,是操作系统的一部分。内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数

image.png

在这五个内存段中,堆和文件映射段的内存是动态分配的。比如说,使用 C 标准库的 malloc() 或者 mmap() ,就可以分别在堆和文件映射段动态分配内存。

对小块内存(小于 128K),C 标准库使用 brk() 来分配,也就是通过移动堆顶的位置来分配内存。这些内存释放后并不会立刻归还系统,而是被缓存起来,这样就可以重复使用。

而大块内存(大于 128K),则直接使用内存映射 mmap() 来分配,也就是在文件映射段找一块空闲内存分配出去。

了解这两种调用方式后,我们还需要清楚一点,那就是,当这两种调用发生后,其实并没有真正分配内存。这些内存,都只在首次访问时才分配,也就是通过缺页异常进入内核中,再由内核来分配内存。

你可能会想到一个问题,如果遇到比页更小的对象,比如不到 1K 的时候,该怎么分配内存呢?

实际系统运行中,确实有大量比页还小的对象,如果为它们也分配单独的页,那就太浪费内存了。

所以,在用户空间,malloc 通过 brk() 分配的内存,在释放时并不立即归还系统,而是缓存起来重复利用。在内核空间,Linux 则通过 slab 分配器来管理小内存。你可以把 slab 看成构建在系统上的一个缓存,主要作用就是分配并释放内核中的小对象。

对内存来说,如果只分配而不释放,就会造成内存泄漏,甚至会耗尽系统内存。所以,在应用程序用完内存后,还需要调用 free() 或 unmap() ,来释放这些不用的内存。

当然,系统也不会任由某个进程用完所有内存。在发现内存紧张时,系统就会通过一系列机制来回收内存,比如下面这三种方式:

1 回收缓存,比如使用 LRU(Least Recently Used)算法,回收最近使用最少的内存页面;

2 回收不常访问的内存,把不常用的内存通过交换分区直接写到磁盘中;

3 杀死进程,内存紧张时系统还会通过 OOM(Out of Memory),直接杀掉占用大量内存的进程。

其中,第二种方式回收不常访问的内存时,会用到交换分区(以下简称 Swap)。Swap 其实就是把一块磁盘空间当成内存来用。它可以把进程暂时不用的数据存储到磁盘中(这个过程称为换出),当进程访问这些内存时,再从磁盘读取这些数据到内存中(这个过程称为换入)。

所以,你可以发现,Swap 把系统的可用内存变大了。不过要注意,通常只在内存不足时,才会发生 Swap 交换。并且由于磁盘读写的速度远比内存慢,Swap 会导致严重的内存性能问题。

第三种方式提到的  OOM(Out of Memory),其实是内核的一种保护机制。它监控进程的内存使用情况,并且使用 oom_score 为每个进程的内存使用情况进行评分:

一个进程消耗的内存越大,oom_score 就越大;

一个进程运行占用的 CPU 越多,oom_score 就越小。

这样,进程的 oom_score 越大,代表消耗的内存越多,也就越容易被 OOM 杀死,从而可以更好保护系统。

当然,为了实际工作的需要,管理员可以通过 /proc 文件系统,手动设置进程的 oom_adj ,从而调整进程的 oom_score。

cat /proc/$pid(具体进程的id)/oom_score

cat /proc/42909/oom_score

这个42909 是php-fpm的oom_score 显示是1  oom_adj 的范围 [-17,15] 数值越大,表示进程越容易被OOM 杀死,数值越小,表示进程越不容易被OOM杀死,其中-17 表示禁止OOM

echo -16 > /proc/$(pidof sshd)/oom_adj   调低进程的oom_score

总结:

内存映射:其实就是将虚拟内存地址映射到物理内存地址。

页:MMU的内存映射的最小单位。也叫逻辑页。大小是4KB

页表: page table 页表是一种特殊的数据结构,放在系统空间的页表区,存放逻辑页与物理页帧的对应关系。

页帧:物理内存中跟逻辑页对应的单位称为页帧。

缺页异常:page fault 分为三种,major page fault、 minor page fault、 invalid(segment fault)。

TLB:转译后备缓冲区–是CPU的一种页表缓存。用来提高虚拟地址转换为物理地址的转换速度。

多级页表:多级页表用来减少页表的项数。

大页:减少由内核加载的映射表的数量。相应的所需的地址转换也减少了,TLB缓存失效的次数就减少了。

虚拟空间分布:

1 只读段,包括代码和常量等。也可以叫做代码段

2 数据段,包括全局变量,静态变量的空间等

3 堆,包括动态分配的内存,从低地址开始向上增长。

4 文件映射段,包括动态库、共享内存等,从高地址开始向下增长。一般是mmap函数所分配的虚拟地址空间

5 栈 包括局部变量和函数调用的上下文等。栈的大小是固定的,一般是8MB  

6 内核空间:内核空间为内核保留,不允许应用程序读写该区域的内容或直接调用内核代码定义的函数。

内存分配:内存分配有 brk  mmap  内存释放有 free 和 unmap

内存回收方式:LRU 算法,swap,oom.

这才是个开始~~ 漫漫长路要走很久的。技术的路上可能会很枯燥,沉下去心。

参考资料:

https://www.linuxprobe.com/linux-kernel-user-space.htmL

https://blog.csdn.net/forDreamYue/article/details/78887035 多级页表原理

https://yq.aliyun.com/articles/55820  page fault