xml地图|网站地图|网站标签 [设为首页] [加入收藏]
父进程和子进程,Linux进程和信号超详细分析
分类:操作系统

父进程

在计算机领域,父进程(英语:Parent Process)指已创建一个或多个子进程的进程。

一 概述

9.1 进程简单说明

进程是一个非常复杂的概念,涉及的内容也非常非常多。在这一小节所列出内容,已经是我极度简化后的内容了,应该尽可能都理解下来,我觉得这些理论比如何使用命令来查看状态更重要,而且不明白这些理论,后面查看状态信息时基本上不知道状态对应的是什么意思。

但对于非编程人员来说,更多的进程细节也没有必要去深究,当然,多多益善是肯定的。

UNIX


在UNIX里,除了进程0(即PID=0的交换进程,Swapper Process)以外的所有进程都是由其他进程使用系统调用fork创建的,这里调用fork创建新进程的进程即为父进程,而相对应的为其创建出的进程则为子进程,因而除了进程0以外的进程都只有一个父进程,但一个进程可以有多个子进程。

操作系统内核以进程标识符(Process Identifier,即PID)来识别进程。进程0是系统引导时创建的一个特殊进程,在其调用fork创建出一个子进程(即PID=1的进程1,又称init)后,进程0就转为交换进程(有时也被称为空闲进程),而进程1(init进程)就是系统里其他所有进程的祖先。

澳门新葡亰手机版, 1.什么是操作系统?

操作系统是人与计算机硬件交互的中介。用户无法直接与计算机硬件交互,无法直接指挥计算机工作,因此需要一个中介,这个中介就是操作系统,用户向操作系统发出命令,由操作系统直接调用硬件工作。

9.1.1 进程和程序的区别

程序是二进制文件,是静态存放在磁盘上的,不会占用系统运行资源(cpu/内存)。

进程是用户执行程序或者触发程序的结果,可以认为进程是程序的一个运行实例。进程是动态的,会申请和使用系统资源,并与操作系统内核进行交互。在后文中,不少状态统计工具的结果中显示的是system类的状态,其实system状态的同义词就是内核状态。

僵尸进程与孤儿进程


当一个子进程结束运行(一般是调用exit、运行时发生致命错误或收到终止信号所导致)时,子进程的退出状态(返回值)会回报给操作系统,系统则以SIGCHLD信号将子进程被结束的事件告知父进程,此时子进程的进程控制块(PCB)仍驻留在内存中。一般来说,收到SIGCHLD后,父进程会使用wait系统调用以获取子进程的退出状态,然后内核就可以从内存中释放已结束的子进程的PCB;而如若父进程没有这么做的话,子进程的PCB就会一直驻留在内存中,也即成为僵尸进程。

孤儿进程则是指父进程结束后仍在运行的子进程。在类UNIX系统中,孤儿进程一般会被init进程所“收养”,成为init的子进程。

为避免产生僵尸进程,实际应用中一般采取的方式是:

  1. 将父进程中对SIGCHLD信号的处理函数设为SIG_IGN(忽略信号);
  2. fork两次并杀死一级子进程,令二级子进程成为孤儿进程而被init所“收养”、清理。

2.Linux是什么?

同Windows操作系统一样,Linux也是一种操作系统,目前绝大多数Web服务器都采用Linux,这也是java程序员熟悉Linux操作系统的原因。

9.1.2 多任务和cpu时间片

现在所有的操作系统都能"同时"运行多个进程,也就是多任务或者说是并行执行。但实际上这是人类的错觉,一颗物理cpu在同一时刻只能运行一个进程,只有多颗物理cpu才能真正意义上实现多任务。

人类会产生错觉,以为操作系统能并行做几件事情,这是通过在极短时间内进行进程间切换实现的,因为时间极短,前一刻执行的是进程A,下一刻切换到进程B,不断的在多个进程间进行切换,使得人类以为在同时处理多件事情。

不过,cpu如何选择下一个要执行的进程,这是一件非常复杂的事情。在Linux上,决定下一个要运行的进程是通过"调度类"(调度程序)来实现的。程序何时运行,由进程的优先级决定,但要注意,优先级值越低,优先级就越高,就越快被调度类选中。除此之外,优先级还影响分配给进程的时间片长短。在Linux中,改变进程的nice值,可以影响某类进程的优先级值。

有些进程比较重要,要让其尽快完成,有些进程则比较次要,早点或晚点完成不会有太大影响,所以操作系统要能够知道哪些进程比较重要,哪些进程比较次要。比较重要的进程,应该多给它分配一些cpu的执行时间,让其尽快完成。下图是cpu时间片的概念。

澳门新葡亰手机版 1 

由此可以知道,所有的进程都有机会运行,但重要的进程总是会获得更多的cpu时间,这种方式是"抢占式多任务处理":内核可以强制在时间片耗尽的情况下收回cpu使用权,并将cpu交给调度类选中的进程,此外,在某些情况下也可以直接抢占当前运行的进程。随着时间的流逝,分配给进程的时间也会被逐渐消耗,当分配时间消耗完毕时,内核收回此进程的控制权,并让下一个进程运行。但因为前面的进程还没有完成,在未来某个时候调度类还是会选中它,所以内核应该将每个进程临时停止时的运行时环境(寄存器中的内容和页表)保存下来(保存位置为内核占用的内存),这称为保护现场,在下次进程恢复运行时,将原来的运行时环境加载到cpu上,这称为恢复现场,这样cpu可以在当初的运行时环境下继续执行。

看书上说,Linux的调度器不是通过cpu的时间片流逝来选择下一个要运行的进程的,而是考虑进程的等待时间,即在就绪队列等待了多久,那些对时间需求最严格的进程应该尽早安排其执行。另外,重要的进程分配的cpu运行时间自然会较多。

调度类选中了下一个要执行的进程后,要进行底层的任务切换,也就是上下文切换,这一过程需要和cpu进程紧密的交互。进程切换不应太频繁,也不应太慢。切换太频繁将导致cpu闲置在保护和恢复现场的时间过长,保护和恢复现场对人类或者进程来说是没有产生生产力的(因为它没有在执行程序)。切换太慢将导致进程调度切换慢,很可能下一个进程要等待很久才能轮到它执行,直白的说,如果你发出一个ls命令,你可能要等半天,这显然是不允许的。

至此,也就知道了cpu的衡量单位是时间,就像内存的衡量单位是空间大小一样。进程占用的cpu时间长,说明cpu运行在它身上的时间就长。注意,cpu的百分比值不是其工作强度或频率高低,而是"进程占用cpu时间/cpu总时间",这个衡量概念一定不要搞错。

Linux

在Linux内核中,进程和POSIX线程有着相当微小的区别,父进程的定义也与UNIX不尽相同。Linux有两种父进程,分别称为(形式)父进程与实际父进程,对于一个子进程来说,其父进程是在子进程结束时收取SIGCHLD信号的进程,而实际父进程则是在多线程环境里实际创建该子进程的进程。对于普通进程来说,父进程与实际父进程是同一个进程,但对于一个以进程形式存在的POSIX线程,父进程和实际父进程可能是不一样的。

 

二 虚拟机

9.1.3 父子进程及创建进程的方式

根据执行程序的用户UID以及其他标准,会为每一个进程分配一个唯一的PID。

父子进程的概念,简单来说,在某进程(父进程)的环境下执行或调用程序,这个程序触发的进程就是子进程,而进程的PPID表示的是该进程的父进程的PID。由此也知道了,子进程总是由父进程创建。

在Linux,父子进程以树型结构的方式存在,父进程创建出来的多个子进程之间称为兄弟进程。CentOS 6上,init进程是所有进程的父进程,CentOS 7上则为systemd。

Linux上创建子进程的方式有三种(极其重要的概念):一种是fork出来的进程,一种是exec出来的进程,一种是clone出来的进程。

(1).fork是复制进程,它会复制当前进程的副本(不考虑写时复制的模式),以适当的方式将这些资源交给子进程。所以子进程掌握的资源和父进程是一样的,包括内存中的内容,所以也包括环境变量和变量。但父子进程是完全独立的,它们是一个程序的两个实例。

(2).exec是加载另一个应用程序,替代当前运行的进程,也就是说在不创建新进程的情况下加载一个新程序。exec还有一个动作,在进程执行完毕后,退出exec所在环境(实际上是进程直接跳转到exec上,执行完exec就直接退出。而非exec加载程序的方式是:父进程睡眠,然后执行子进程,执行完后回到父进程,所以不会立即退出当前环境)。所以为了保证进程安全,若要形成新的且独立的子进程,都会先fork一份当前进程,然后在fork出来的子进程上调用exec来加载新程序替代该子进程。例如在bash下执行cp命令,会先fork出一个bash,然后再exec加载cp程序覆盖子bash进程变成cp进程。但要注意,fork进程时会复制所有内存页,但使用exec加载新程序时会初始化地址空间,意味着复制动作完全是多余的操作,当然,有了写时复制技术不用过多考虑这个问题。

(3).clone用于实现线程。clone的工作原理和fork相同,但clone出来的新进程不独立于父进程,它只会和父进程共享某些资源,在clone进程的时候,可以指定要共享的是哪些资源。

题外知识:如何创建一个子进程?

每次fork一个进程的时候,虽然调用一次fork(),但却返回两次:子进程的返回值为0,父进程的返回值为子进程的pid。所以,可以使用下面的shell伪代码来描述运行一个ls命令时的过程:

fpid=`fork()`
if [ $fpid = 0){
    exec(ls) || echo "Can't exec ls"
}
wait($fpid)

假设上面是在shell脚本中执行ls命令,那么fork的是shell脚本进程。fork后,检测到fpid=0,表示fork子进程成功了,于是执行exec(ls),当ls执行结束,将继续执行到wait,也就是回到了shell脚本进程继续执行后续操作。如果不是fork,也就是$fpid不为0,说明这是父进程,也就是shell脚本自身进程,它不会进入if语句,而是直接执行后续程序。

如果在这个shell脚本中某个位置,执行exec命令(exec命令调用的其实就是exec家族函数),shell脚本进程直接切换到exec命令上,执行完exec命令,就表示进程终止,于是exec命令后面的所有命令都不会再执行。

一般情况下,兄弟进程之间是相互独立、互不可见的,但有时候通过特殊手段,它们会实现进程间通信。例如管道协调了两边的进程,两边的进程属于同一个进程组,它们的PPID是一样的,管道使得它们可以以"管道"的方式传递数据。

进程是有所有者的,也就是它的发起者,某个用户如果它非进程发起者、非父进程发起者、非root用户,那么它无法杀死进程。且杀死父进程(非终端进程),会导致子进程变成孤儿进程,孤儿进程的父进程总是init/systemd。

子进程


在计算机领域中,子进程为由另外一个进程(对应称之为父进程)所创建的进程。子进程继承了父进程的大部分属性,例如文件描述符。

1.什么是虚拟机?

在一台物理计算机上使用软件模拟建立的、具有真实计算机逻辑因素的计算机,由于该计算机没有独立的物理构成,因此称作虚拟机。

9.1.4 进程的状态

进程并非总是处于运行中,至少cpu没运行在它身上时它就是非运行的。进程有几种状态,不同的状态之间可以实现状态切换。下图是非常经典的进程状态描述图,个人感觉右图更加易于理解。

 澳门新葡亰手机版 2澳门新葡亰手机版 3

运行态:进程正在运行,也即是cpu正在它身上。

就绪(等待)态:进程可以运行,已经处于等待队列中,也就是说调度类下次可能会选中它

睡眠(阻塞)态:进程睡眠了,不可运行。

各状态之间的转换方式为:(也许可能不太好理解,可以结合稍后的例子)

(1)新状态->就绪态:当等待队列允许接纳新进程时,内核便把新进程移入等待队列。

(2)就绪态->运行态:调度类选中等待队列中的某个进程,该进程进入运行态。

(3)运行态->睡眠态:正在运行的进程因需要等待某事件(如IO等待、信号等待等)的出现而无法执行,进入睡眠态。

(4)睡眠态->就绪态:进程所等待的事件发生了,进程就从睡眠态排入等待队列,等待下次被选中执行。

(5)运行态->就绪态:正在执行的进程因时间片用完而被暂停执行;或者在抢占式调度方式中,高优先级进程强制抢占了正在执行的低优先级进程。

(6)运行态->终止态:一个进程已完成或发生某种特殊事件,进程将变为终止状态。对于命令来说,一般都会返回退出状态码。

注意上面的图中,没有"就绪-->睡眠"和"睡眠-->运行"的状态切换。这很容易理解。对于"就绪-->睡眠",等待中的进程本就已经进入了等待队列,表示可运行,而进入睡眠态表示暂时不可运行,这本身就是冲突的;对于"睡眠-->运行"这也是行不通的,因为调度类只会从等待队列中挑出下一次要运行的进程。

再说说运行态-->睡眠态。从运行态到睡眠态一般是等待某事件的出现,例如等待信号通知,等待IO完成。信号通知很容易理解,而对于IO等待,程序要运行起来,cpu就要执行该程序的指令,同时还需要输入数据,可能是变量数据、键盘输入数据或磁盘文件中的数据,后两种数据相对cpu来说,都是极慢极慢的。但不管怎样,如果cpu在需要数据的那一刻却得不到数据,cpu就只能闲置下来,这肯定是不应该的,因为cpu是极其珍贵的资源,所以内核应该让正在运行且需要数据的进程暂时进入睡眠,等它的数据都准备好了再回到等待队列等待被调度类选中。这就是IO等待。

其实上面的图中少了一种进程的特殊状态——僵尸态。僵尸态进程表示的是进程已经转为终止态,它已经完成了它的使命并消逝了,但是内核还没有来得及将它在进程列表中的项删除,也就是说内核没给它料理后事,这就造成了一个进程是死的也是活着的假象,说它死了是因为它不再消耗资源,调度类也不可能选中它并让它运行,说它活着是因为在进程列表中还存在对应的表项,可以被捕捉到。僵尸态进程并不占用多少资源,它仅在进程列表中占用一点点的内存。大多数僵尸进程的出现都是因为进程正常终止(包括kill -9),但父进程没有确认该进程已经终止,所以没有通告给内核,内核也就不知道该进程已经终止了。僵尸进程更具体说明见后文。

另外,睡眠态是一个非常宽泛的概念,分为可中断睡眠和不可中断睡眠。可中断睡眠是允许接收外界信号和内核信号而被唤醒的睡眠,绝大多数睡眠都是可中断睡眠,能ps或top捕捉到的睡眠也几乎总是可中断睡眠;不可中断睡眠只能由内核发起信号来唤醒,外界无法通过信号来唤醒,主要表现在和硬件交互的时候。例如cat一个文件时,从硬盘上加载数据到内存中,在和硬件交互的那一小段时间一定是不可中断的,否则在加载数据的时候突然被人为发送的信号手动唤醒,而被唤醒时和硬件交互的过程又还没完成,所以即使唤醒了也没法将cpu交给它运行,所以cat一个文件的时候不可能只显示一部分内容。而且,不可中断睡眠若能被人为唤醒,更严重的后果是硬件崩溃。由此可知,不可中断睡眠是为了保护某些重要进程,也是为了让cpu不被浪费。一般不可中断睡眠的存在时间极短,也极难通过非编程方式捕捉到。

其实只要发现进程存在,且非僵尸态进程,还不占用cpu资源,那么它就是睡眠的。包括后文中出现的暂停态、追踪态,它们也都是睡眠态。

产生


在Unix中,子进程通常为系统调用fork的产物。在此情况下,子进程一开始就是父进程的副本,而在这之后,根据具体需要,子进程可以借助exec调用来链式加载另一程序。

2.虚拟机的价值

可以在一台物理计算机建立多个逻辑上的计算机,即虚拟机,每个虚拟机相互独立,从而利用一台物理计算机创作出多台计算机的情况,为多机测试搭建环境。

9.1.5 举例分析进程状态转换过程

进程间状态的转换情况可能很复杂,这里举一个例子,尽可能详细地描述它们。

以在bash下执行cp命令为例。在当前bash环境下,处于可运行状态(即就绪态)时,当执行cp命令时,首先fork出一个bash子进程,然后在子bash上exec加载cp程序,cp子进程进入等待队列,由于在命令行下敲的命令,所以优先级较高,调度类很快选中它。在cp这个子进程执行过程中,父进程bash会进入睡眠状态(不仅是因为cpu只有一颗的情况下一次只能执行一个进程,还因为进程等待),并等待被唤醒,此刻bash无法和人类交互。当cp命令执行完毕,它将自己的退出状态码告知父进程,此次复制是成功还是失败,然后cp进程自己消逝掉,父进程bash被唤醒再次进入等待队列,并且此时bash已经获得了cp退出状态码。根据状态码这个"信号",父进程bash知道了子进程已经终止,所以通告给内核,内核收到通知后将进程列表中的cp进程项删除。至此,整个cp进程正常完成。

假如cp这个子进程复制的是一个大文件,一个cpu时间片无法完成复制,那么在一个cpu时间片消耗尽的时候它将进入等待队列。

假如cp这个子进程复制文件时,目标位置已经有了同名文件,那么默认会询问是否覆盖,发出询问时它等待yes或no的信号,所以它进入了睡眠状态(可中断睡眠),当在键盘上敲入yes或no信号给cp的时候,cp收到信号,从睡眠态转入就绪态,等待调度类选中它完成cp进程。

在cp复制时,它需要和磁盘交互,在和硬件交互的短暂过程中,cp将处于不可中断睡眠。

假如cp进程结束了,但是结束的过程出现了某种意外,使得bash这个父进程不知道它已经结束了(此例中是不可能出现这种情况的),那么bash就不会通知内核回收进程列表中的cp表项,cp此时就成了僵尸进程。

与父进程的关系


一个进程可能下属多个子进程,但最多只能有1个父进程,而若某一进程没有父进程,则可知该进程很可能由内核直接生成。在Unix与类Unix系统中,进程ID为1的进程(即init进程)是在系统引导阶段由内核直接创建的,且不会在系统运行过程中终止执行(可参见Linux启动流程);而对于其他无父进程的进程,则可能是为在用户空间完成各种后台任务而执行的。

当某一子进程结束、中断或恢复执行时,内核会发送SIGCHLD信号予其父进程。在默认情况下,父进程会以SIG_IGN函数忽略之。

三 常用操作

9.1.6 进程结构和子shell

  • 前台进程:一般命令(如cp命令)在执行时都会fork子进程来执行,在子进程执行过程中,父进程会进入睡眠,这类是前台进程。前台进程执行时,其父进程睡眠,因为cpu只有一颗,即使是多颗cpu,也会因为执行流(进程等待)的原因而只能执行一个进程,要想实现真正的多任务,应该使用进程内多线程实现多个执行流。
  • 后台进程:若在执行命令时,在命令的结尾加上符号"&",它会进入后台。将命令放入后台,会立即返回父进程,并返回该后台进程的的jobid和pid,所以后台进程的父进程不会进入睡眠。当后台进程出错,或者执行完成,总之后台进程终止时,父进程会收到信号。所以,通过在命令后加上"&",再在"&"后给定另一个要执行的命令,可以实现"伪并行"执行的方式,例如"cp /etc/fstab /tmp & cat /etc/fstab"。
  • bash内置命令:bash内置命令是非常特殊的,父进程不会创建子进程来执行这些命令,而是直接在当前bash进程中执行。但如果将内置命令放在管道后,则此内置命令将和管道左边的进程同属于一个进程组,所以仍然会创建子进程。

说到这了,应该解释下子shell,这个特殊的子进程。

一般fork出来的子进程,内容和父进程是一样的,包括变量,例如执行cp命令时也能获取到父进程的变量。但是cp命令是在哪里执行的呢?在子shell中。执行cp命令敲入回车后,当前的bash进程fork出一个子bash,然后子bash通过exec加载cp程序替代子bash。请不要在此纠结子bash和子shell,如果搞不清它们的关系,就当它是同一种东西好了。

那是否可以理解为所有命令、脚本其运行环境都是在子shell中呢?显然,上面所说的bash内置命令不是在子shell中运行的。其他的所有方式,都是在子shell中完成,只不过方式不尽相同。

分为几种情况:

  • ①.执行bash内置命令:bash内置命令是非常特殊的,父进程不会创建子进程来执行这些命令,而是直接在当前bash进程中执行。但如果将内置命令放在管道后,则此内置命令将和管道左边的进程同属于一个进程组,所以仍然会创建子进程,但却不一定是子shell。请先阅读完下面的几种情况再来考虑此项。
  • ②.执行bash命令本身:这是一个很巧合的命令。bash命令本身是bash内置命令,在当前shell环境下执行内置命令本不会创建子shell,也就是说不会有独立的bash进程出现,而实际结果则表现为新的bash是一个子进程。其中一个原因是执行bash命令会加载各种环境配置项,为了父bash的环境得到保护而不被覆盖,所以应该让其以子shell的方式存在。虽然fork出来的bash子进程内容完全继承父shell,但因重新加载了环境配置项,所以子shell没有继承普通变量,更准确的说是覆盖了从父shell中继承的变量。不妨试试在/etc/bashrc文件中定义一个变量,再在父shell中export名称相同值却不同的环境变量,然后到子shell中看看该变量的值为何?
    • 其实执行bash命令,即可以认为是进入了子shell,也可以认为没有进入子shell。从bash是内置命令的角度来考虑,它不会进入子shell,这一点在执行bash命令后从变量$BASH_SUBSHELL的值为0可以验证出来。但从执行bash命令后进入了新的shell环境来看,它有其父bash进程,所以它算是进入了子shell。
  • ③.执行shell脚本:因为脚本中第一行总是"#!/bin/bash"或者直接"bash xyz.sh",所以这和上面的执行bash进入子shell其实是一回事,都是使用bash命令进入子shell。只不过此时的bash命令和情况②中直接执行bash命令所隐含的选项不一样,所以继承和加载的shell环境也不一样。事实也确实如此,shell脚本只会继承父shell的一项属性:父进程所存储的各命令的路径。
    • 另外,执行shell脚本有一个动作:命令执行完毕后自动退出子shell。
  • ④.执行非bash内置命令:例如执行cp命令、grep命令等,它们直接fork一份bash进程,然后使用exec加载程序替代该子bash。此类子进程会继承所有父bash的环境。但严格地说,这已经不是子shell,因为exec加载的程序已经把子bash进程替换掉了,这意味着丢失了很多bash环境。
  • ⑤.非内置命令的命令替换:当命令行中包含了命令替换部分时,将开启一个子shell先执行这部分内容,再将执行结果返回给当前命令。因为这次的子shell不是通过bash命令进入的子shell,所以它会继承父shell的所有变量内容。这也就解释了"$(echo $$)"中"$$"的结果是当前bash的pid号,而不是子shell的pid号,因为它不是使用bash命令进入的子shell。
  • ⑥.使用括号()组合一系列命令:例如(ls;date;echo haha),独立的括号将会开启一个子shell来执行括号内的命令。这种情况等同于情况⑤。

最后需要说明的是,子shell的环境设置不会粘滞到父shell环境,也就是说子shell的变量等不会影响父shell。

还有两种特殊的脚本调用方式:exec和source。

  • exec:exec是加载程序替换当前进程,所以它不开启子shell,而是直接在当前shell中执行命令或脚本,执行完exec后直接退出exec所在的shell。这就解释了为何bash下执行cp命令时,cp执行完毕后会自动退出cp所在的子shell。
  • source:source一般用来加载环境配置类脚本。它也不会开启子shell,直接在当前shell中执行调用脚本且执行脚本后不退出当前shell,所以脚本会继承当前已有的变量,且脚本执行完毕后加载的环境变量会粘滞给当前shell,在当前shell生效。

本文由澳门新葡亰手机版发布于操作系统,转载请注明出处:父进程和子进程,Linux进程和信号超详细分析

上一篇:没有了 下一篇:linux一步一脚印,Redis阅读目录
猜你喜欢
热门排行
精彩图文