Linux 进程

引入进程的原因

进程是为了刻画并发程序的执行过程而引入的概念,进程管理就是对并发程序的运行过程的管理,也就是对CPU的管理。

进程管理的目标是最大限度地发挥CPU的处理能力,提高进程的运行效率。

并发执行

程序的并发执行是指若干程序或程序段同时运行。它们的执行在时间上是重叠的。

程序在并发执行时会导致执行结果的不可再现性,这是多道程序系统必须解决的问题。

进程与程序

进程与程序的概念既相互关联又相互区别。程序是进程的一个组成部分,是进程的执行文本,而进程是程序的执行过程。

  1. 一个进程可以顺序执行多个程序;
  2. 一个程序可以对应多个进程。

进程的特性

  1. 动态性。进程由“创建”而产生,由“撤销”而消亡,因“调度”而运行,因“等待”而停顿。进程从创建到消失的全过程称为进程的生命周期。
  2. 并发性。在同一时间段内有多个进程在系统中活动。它们宏观上是在并发运行,而微观上是在交替运行。
  3. 独立性。进程是可以独立运行的基本单位,是操作系统分配资源和调度管理的基本对象。因此,每个进程都独立地拥有各种必要的资源,独立地占有CPU运行。
  4. 异步性。每个进程都独立地执行,各自按照不可预知的速度向前推进。进程之间的协调运行由操作系统负责。

进程的基本状态

  1. 就绪态。进程已经分配到了除CPU之外的所有资源,这时的进程状态称为就绪状态。处于就绪态的进程,一旦获得CPU便可立即执行。系统中通常会有多个进程处于就绪态,它们排成一个就绪队列。
  2. 运行态。进程已经获得CPU,正在运行,这时的进程状态称为运行态。在单CPU系统中,任何时刻只能有一个进程处于运行态。
  3. 等待态。进程因某种资源不能满足,或希望的某事件尚未发生而暂停执行时,则称它处于等待态。系统中常常会有多个进程处于等待态,它们按等待的事件分类,排成多个等待队列。

进程控制块

进程由程序、数据和进程控制块三个基本部分组成。

  • 程序是进程执行的可执行代码;
  • 数据是进程所处理的对象;
  • 进程控制块用于记录有关进程的各种信息。

进程控制块(Process Control Block,PCB)是为管理进程而设置的一个数据结构,用于记录进程的相关信息。PCB是进程的代表,PCB存在则进程就存在,PCB消失则进程也就结束了。在进程的生存期中,系统通过PCB来感知进程,了解它的活动情况,通过它对进程实施控制和调度。

  1. 进程描述信息。

    了解进程的权限,以及确定这个进程与其他进程之间的关系。系统通过PID来标识各个进程。

  2. 进程控制与调度信息。

    进程控制块记录了进程的当前状态、调度策略、优先级、时间片等信息。系统依据这些信息实施进程的控制与调度。

  3. 资源信息。

    进程的地址空间、要访问的文件和设备以及要处理的信号等。进程是系统分配资源的基本单位。

  4. 现场信息。

    进程现场也称为进程上下文(process context),包括CPU的各个寄存器的值。这些值刻画了进程的运行状态和环境。退出CPU的进程必须保存好这些现场信息,以便在下次被调度时继续运行。

Linux系统中的进程

可执行态(包括运行态与就绪态)、可中断睡眠态、不可中断睡眠态、暂停态和僵死态。

Linux系统用task_struct结构来记录进程的信息,称为进程描述符。

ps        # 默认格式本终端进程
ps  -ef   # 全格式所有进程
ps  -aux  # 用户格式所有进程

进程的运行模式

x86/x64 CPU的执行模式

在CPU中设置了一个代表特权级别的状态字(即cs寄存器中的DPL字段),修改这个状态字就可以切换CPU的运行模式。

  • 特权指令

    可以访问系统中所有寄存器、内存单元和IO端口,修改系统的关键设置。例如向控制寄存器加载地址、清理内存、设置时钟、进入中断等。

  • 非特权指令

    只能访问用户程序自己的内存地址空间。

x86/x64的CPU支持4种不同特权级别,Linux系统只用到了其中两个

  • 核心态:最高特权级模式ring0

    访问全部的内存地址

  • 用户态:最低特权级模式ring3

    访问受限的地址空间

从进程的角度看,内核的功能有两个:

  1. 一是支持进程的运行,包括为进程分配资源,控制和调度进程的运行;
  2. 二是为进程提供服务,也就是提供一些内核函数(称为系统调用)供进程调用。

Linux系统的内核

用户程序则只能运行在用户态。从用户态转换为核心态的唯一途径是中断(包括陷入)。

进程的资源

资源的用途不同,分配策略也就不同。

  • 内存、文件和设备资源是按需分配,即用时分配,用完即回收;
  • 地址空间和信号是进程执行的必要资源,它们在进程创建时分配,在进程的整个运行期间都一直占有;
  • 内核栈属于进程的固有资源,它和进程描述符一样,在进程创建时分配,并保持在进程的整个存在期间。也就是说即使是僵尸进程也会保有它的描述符和内核栈。

进程的地址空间

进程有用户态和核心态两种运行模式,在不同的模式下可访问的地址空间也不相同。进程的地址空间被划分为用户空间和内核空间两部分。

进程的文件与设备

文件是信息的长久保存形式,应用程序经常要使用或处理文件。此外,应用程序还需要使用设备来与外界传输数据。因此文件和设备都是进程的常用资源。在Linux系统中设备被当作文件来处理,因此两者都由文件系统来管理

进程的信号通信

为实现信号通信,进程需要拥有信号队列以及信号处理程序。

进程的描述结构

严格地说,thread_info与task_struct合起来才是一个完整的PCB。

进程的组织和控制

管理进程就是管理进程的PCB。

数组、散列表和链表3种方式的综合以求达到最好的效率。

  1. 进程链表
  2. PID散列表
  3. 进程树链表
  4. 可执行队列
  5. 等待队列

进程控制是指对进程的生命周期进行有效的管理,实现进程的创建、撤销以及进程各状态之间的转换等控制功能。进程控制的目标是使多进程能够平稳高效地并发执行充分共享系统资源。

进程控制的任务主要有以下几个:

  1. 创建进程。

    创建PCB,分配资源链入进程链表和可执行队列中。

  2. 撤销进程。

    摘出进程队列及链表中,释放资源,销去PCB。

  3. 阻塞进程。从运行态到等待态的转换。

    保存现场,PCB插入等待队列中,调用进程调度程序,从可执行队列中选择一个进程投入运行。

  4. 唤醒进程。从等待态转换到就绪态。

    在等待队列中找到满足唤醒条件的进程,PCB插入可执行队列。

Linux系统的进程控制

控制进程在整个生命周期中各种状态之间的转换(不包括就绪态与运行态之间的转换,那是由进程调度来实现的)。

进程的创建与映像更换

采用克隆(clone)的方法,用先复制再变身的两个步骤来创建进程,即先按照父进程创建一个子进程,再更换子进程的映像。

调用 exec() 来更换进程映像,变换为一个全新的进程。

写时复制技术

fork() 完成后并不立刻复制父进程的映像内容,而是让子进程共享父进程的同一个复本,直到遇到有一方执行写入操作(即修改了映像)时才进行复制,使父子进程拥有各自独立的复本。也就是说,复制操作被推迟到真正需要的时候才进行

进程的终止与等待

exit()

  • 释放进程所占有的资源,只保留其描述符和内核栈;
  • 向进程描述符中写入进程退出码和一些统计信息;
  • 设置进程状态为“僵死态”;
  • 如果有子进程的话就为它们找一个新的父进程来“领养”;
  • 通知父进程回收自己;
  • 调用进程调度程序切换进程。

wait()

进程的阻塞与唤醒

进程通过调用内核函数来阻塞自己。

Shell命令的执行过程

进程调度

进程调度的功能是按照一定的策略把CPU分配给就绪进程,使它们轮流地使用CPU运行。进程调度实现了进程就绪态与运行态之间的转换

当正在运行的进程因某种原因放弃CPU时,进程调度程序就会被调用。

调度操作包括以下两项内容:

  1. 选择进程:即按一定的调度算法,从就绪进程队列中选一个进程。
  2. 切换进程:为换下的进程保留现场,为选中的进程恢复现场,使其运行。

通常的策略是:

  1. 实时进程(如视频播放、机器人控制、实时数据采集等)要求系统即时响应;
  2. 交互式进程(如 Shell、文本编辑、桌面系统等)需要及时响应;
  3. 后台批处理进程(如系统清理、银行轧账等)允许延缓响应。

常用的调度算法有以下几种:

  1. 先进先出法。按照进程在可执行队列中的先后次序来调度。这是最简单的调度法,但缺点是对一些紧迫任务的响应时间过长。
  2. 短进程优先法。优先调度短进程运行,以提高系统的吞吐量,但对长进程不利。
  3. 时间片轮转法。进程按规定的时间片轮流使用CPU。这种方法可满足系统对用户响应时间的要求,有很好的公平性。时间片长度的选择应适当,过短会引起频繁的进程调度,过长则对用户的响应较慢。
  4. 优先级调度法。为每个进程设置优先级,调度时优先选择优先级高的进程运行,使紧迫的任务可以优先得到处理。更为细致的调度法又将优先级分为静态优先级和动态优先级。静态优先级是预先指定的,动态优先级则随进程的运行时间而降低或升高。两种优先级组合调度,既可以保证对高优先级进程的响应,也不致过度忽略低优先级的进程。

实际应用中,经常是多种策略结合使用。如时间片轮转法中也可适当考虑优先级因素,对于紧急的进程可以分配一个长一些的时间片,或连续运行多个时间片等。

Linux系统的进程调度

Linux的进程描述符中记录了与进程调度相关的信息:

  1. 调度策略(policy)

    Linux 将进程分为实时进程与普通(非实时)进程两类,分别采用不同的调度策略。实时进程采用SCHED_FIFO或SCHED_RR调度策略,普通进程采用SCHED_NORMAL普通调度策略。

  2. 实时优先级(rt_priority)

    取值范围为0(最低)~99(最高)。实时进程此项为1~99,非实时进程此项为0。默认MAX_RT_PRIO配置为100。

  3. 静态优先级(static_prio)

    “nice数”(范围是 -20~19),它决定了进程的静态优先级。

    普通进程的static_prio取值范围为100(最高)~139(最低),默认为120;实时进程的此项无实际意义。

    static_prio = MAX_RT_PRIO + nice + 20

  4. 动态优先级(prio)

    普通进程的prio的取值范围为100(最高)~139(最低),可以在需要时变化;实时进程的prio值为99 - rt_priority,取值范围为0(最高)~99(最低),不再变化。

  5. 时间片(time_slice)

    进程当前剩余的时间。普通进程的时间片的初始大小取决于进程的静态优先级(static_prio),取值范围为5~800ms,优先级越高则时间片越长。

实时进程的优先级要高于普通进程。

实时进程的调度算法比较简单,基本原则就是按优先级调度。

普通进程的调度算法则比较复杂,O(1)调度器的负载性能极佳,但在公平性上做得不够好,因此不久就被“完全公平调度器”(Completely Fair Scheduler,CFS)

用户抢占和内核抢占

用户级的抢占

调度的方式分为主动调度与抢占调度两种。主动调度就是由进程直接调用schedule()函数,主动放弃CPU;抢占调度是由内核调用schedule()函数,强制切换进程。抢占调度又分为周期性抢占与优先级抢占。周期性抢占是在每个时钟节拍中判断是否需要切换进程,如果需要就通知内核重新调度。优先级抢占是当有高优先级的进程就绪时,内核将中断当前进程,调度更高优先级的进程运行。

抢占的时机是在进程从核心态返回到用户态时。

内核抢占

内核抢占允许运行在核心态下的进程主动放弃CPU,或是在可以保证内核代码安全的前提下被其他进程抢占。

抢占的时机是在中断处理结束返回内核空间时。内核抢占使得系统的实时性能显著提高,这对嵌入式系统来说更为重要。

实时进程的调度

  • 先进先出法(SCHED_FIFO)

    调度程序依次选择当前最高优先级的FIFO类型的进程,调度其运行。

  • 时间片轮转法(SCHED_RR)

    给每个实时进程分配一个时间片,然后按照它们的优先级加入到相应的优先级队列中。

  • 最后期限法(SCHED_DEADLINE)

普通进程的调度

在新内核中,普通(SCHED_NORMAL)调度法就是完全公平调度法(CFS)。

  • 将CPU的使用权按比例分配给各就绪进程。

    负载权重(load weight)。每个进程都有一个负载权重,这个权重是根据进程的静态优先级计算而来的。从 100~139,优先级越高(优先数越小)则权重越大,每级递增约25%。进程的权重与当前所有就绪进程的权重总和之比就是该进程的权重比,这个权重比就是该进程应获得的CPU使用比例。

  • 根据进程的理想时间公平地调度各进程运行。

    虚拟时钟( vruntime)。每个进程都设有一个虚拟时钟,用于计量进程已消耗的CPU时间。虚拟时钟所计量的是虚拟运行时间,是对实际运行时间加权后的结果。因此虚拟时钟的前进步调可能快于或慢于实际时钟,这取决于进程的权重,或者说优先级。默认优先级进程的虚拟时钟与实际时钟的步调相同。优先级低的进程其虚拟时钟的步调较快,而优先级高的步调则较慢,每级递减约25%。

切换的条件是当前进程的实际运行时间超过了它的理想运行时间,或它的虚拟运行时间大于就绪队列中的最小vruntime值。

Linux系统的进程切换

切换操作包括两个步骤:

  1. 切换进程的地址空间;

  2. 切换进程的执行环境,包括内核栈和CPU硬件环境。

    • 程序执行环境。与程序执行相关的寄存器包括通用寄存器、段寄存器、指令地址寄存器和标志寄存器。

      当进程的运行态改变时需要切换它的程序执行环境。

    • 硬件执行环境。与CPU运作相关的寄存器包括控制寄存器、调试寄存器以及一些有关浮点计算及CPU模式设定的寄存器。此外还有一些CPU所使用的系统表和数据区。

      在进程切换时需要切换硬件执行环境

进程切换操作主要依靠 thread 结构完成。thread 中保存了所有硬件现场的数据,除此之外还保存了esp和eip两个寄存器的值,作为恢复运行时的初始值。

进程的互斥与同步

为保证进程不因竞争资源而导致错误的执行结果,需要通过某种手段实现相互制约。

  • 进程的同步,即相关进程为协作完成同一任务而引起的直接制约关系;

  • 进程的互斥,即进程间因竞争系统资源而引起的间接制约关系。

临界资源与临界区

临界资源(critical resource)是一次仅允许一个进程使用的资源。

临界区 ( critical region)是程序中访问临界资源的程序片段。

进程的互斥(mutex)就是禁止多个进程同时进入各自的访问同一临界资源的临界区,以保证对临界资源的排他性使用。

进程的同步(synchronization)是指进程间为合作完成一个任务而互相等待、协调步调。

同步是一种更为复杂的互斥,而互斥是一种特殊的同步。广义地讲,互斥与同步实际上都是一种同步机制。

互斥与同步的实现方法

  • 原子操作是用特定汇编语言实现的操作,它的操作过程受硬件的支持,具有不可分割性。原子操作主要用于实现资源的计数。
  • 互斥锁机制通过给资源加/解锁的方式来实现互斥访问资源,主要用于数据的保护。
  • 信号量是一种比互斥锁更为灵活的机制,主要用于进程的同步。

死锁问题

死锁(deadlock)是指系统中若干进程相互“无知地”等待对方所占有的资源而无限地处于等待状态的一种僵持局面,其现象是若干进程均停顿不前,且无法自行恢复。

死锁是并发进程因相互制约不当而造成的最严重后果,是并发系统的潜在隐患。一旦发生死锁,通常采取的措施是强制地撤销一个或几个进程,释放它们占用的资源。这些进程将前功尽弃,因而死锁是对系统资源极大的浪费。

死锁的根本原因是系统资源有限,而多个并发进程因竞争资源而相互制约。相互制约的进程需要彼此等待,在极端情况下,就可能出现死锁。

产生死锁的4个必要条件:

  • 资源的独占使用。资源由占有者独占,不允许其他进程同时使用。
  • 资源的非抢占式分配。资源一旦分配就不能被剥夺,直到占用者使用完毕释放。
  • 对资源的保持和请求。进程因请求资源而被阻塞时,对已经占有的资源保持不放。
  • 对资源的循环等待。每个进程已占用一些资源,而又等待别的进程释放资源。

进程通信

然而每个进程都只在自己独立的地址空间中运行,无法直接访问其他进程的空间。当进程间需要交换数据时,必须采用某种特定的手段,这就是进程通信。

  • 信号量(semaphore)。信号量分为内核信号量与IPC信号量。IPC信号量是用户态进程使用的同步与互斥机制。
  • 信号( signal)。信号是进程间可互相发送的控制信息,一般只是几个字节的数据,用于通知进程有某个事件发生。信号属于低级进程通信,传递的信息量小,但它是Linux进程天生具有的一种通信能力,即每个进程都具有接收信号和处理信号的能力。
  • 管道(pipe)。管道是连接两个进程的一个数据传输通路,一个进程向管道写数据,另一个进程从管道读数据,实现两进程之间同步传递字节流。管道的信息传输量大,速度快,内置同步机制,使用简单。
  • 消息队列(message queue)。消息是结构化的数据,消息队列是由消息链接而成的链式队列。进程之间通过消息队列来传递消息,有写权限的进程可以向队列中添加消息,有读权限的进程则可以读走队列中的消息。与管道不同的是,这是一种异步的通信方式。
  • 共享内存( shared-memory)。共享内存通信方式就是在内存中开辟一段存储区,将这个区映射到多个进程的地址空间中,使得多个进程共享这个内存区。

Linux信号通信原理

信号是一组正整数常量,进程之间通过传送信号来通信,通知进程发生了某事件。

发信号常用的系统调用是 kill()。当有信号产生时,内核就把信号的相关信息保存到目标进程的信号队列中。这些已经存入信号队列但还没有被处理的信号称为悬挂信号( pending signal)

每当一个进程即将从核心态返回用户态时,内核都要检查该进程是否有未被阻塞的悬挂信号。do_signal() 的工作就是将信号从信号队列中提取出来,交付信号处理程序进行处理。

信号的处理方式分为以下3种:

  • 忽略(ignore):不做任何处理。
  • 默认(default):调用内核的默认处理函数来处理信号。
  • 捕获(catch):执行进程自己设置的信号处理函数。

SIGSTOP和SIGKILL这两个信号必须按默认的操作进行处理,即停止或终止进程。

Linux管道通信原理

管道通信具有以下特点:

  • 管道是单向的,即数据只能单向传输。需要双向通信时,要建立两个管道。
  • 管道的容量是有限的,通常是一个内存页面大小。
  • 管道所传送的是无格式字节流,使用管道的双方必须事先约定好数据的格式。

线程

线程是构成进程的可独立运行的单元,是进程内的一个执行流。一个进程可以由一个或多个线程构成,并以线程作为调度实体,占有CPU运行。此时的进程可以看作是一个容器,它可以容纳多个线程,为它们的运行提供所有必要的资源。进程内的所有线程共享进程拥有的资源,分别按照各自的路径执行。

线程与进程的比较

  • 在资源分配方面,进程是操作系统资源分配的基本单位。
  • 在CPU调度方面,线程是调度执行的基本单位。
  • 在通信方面,多个线程共享同一内存地址空间

内核级线程与用户级线程

线程机制主要有“用户级线程”与“内核级线程”之分,区别在于线程的调度是在核内还是在核外进行的。

  • 用户级线程不需要内核支持,对线程的管理和调度完全由用户程序完成。
  • 内核级线程则是由内核完成对线程的管理和调度工作。

内核级线程也更利于并发使用多CPU资源。要支持内核级线程,操作系统内核需要设置描述线程的数据结构,提供独立的线程管理方案和专门的线程调度程序。

用户线程不需要额外的内核开销,内核的实现相对简单得多,同时还节省了系统进行线程管理的时间和空间开销。

Linux系统的线程

内核把线程当作一种特殊的进程来看待,它和普通的进程一样,只不过该进程要和其他一些进程共享地址空间和信号等资源。

clone() 允许在调用时多传递一些参数标志来指明需要共享的资源。

一个进程的所有线程构成一个线程组(在Linux中也称之为进程组),其中第一个创建的线程是领头线程,领头线程的PID 就作为该线程组的组标识号TGID。

相关库函数

创建进程

system()

int system(const char *string)

#include <stdlib.h>

int main()
{
    system("ps -aux");
}

如同在终端中使用 sh -c 'ps -aux' 命令。这种方法会启动一个 shell,因此效率不高,很少使用。

fork()

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

void pinfo(const char *info, pid_t pid)
{
    printf("%s\tp: %d\tpid: %d\tppid: %d\n", info, pid, getpid(), getppid());
}

int main(int argc, char *argv[])
{
    pid_t p1 = fork();
    if (p1 == -1) {
        perror("fork");
        return -1;
    }
    pinfo("p1", p1);

    pid_t p2 = fork();
    if (p2 == -1) {
        perror("fork");
        return -1;
    }
    pinfo("p2", p2);
    return 0;
}

exec() 系列

int execl(const char *__path, const char *__arg, ...);
int execlp(const char *__file, const char *__arg, ...);
int execle(const char *__path, const char *__arg, ...);

int execv(const char *__path, char *const __argv[]);
int execvp (const char *__file, char *const __argv[]);
int execve(const char *__path, char *const *__argv, char *const *__envp);

没有特殊结尾的版本需要在第一个参数中给出可执行文件的路径;
p 结尾的版本会在 PATH 中搜索第一个参数给出的可执行文件;
e 结尾的版本会把 envp 中给出的值传递到环境中;

参数不定的 execl 和参数固定的 execvarg, argv 参数都需要以 NULL 结尾。

#include <unistd.h>

char *args[] = {"ps", "-aux", NULL};
char *envp[] = {"aaa=456", "bbb=123", NULL};

int foo(int i)
{
    switch (i) {
        case 0:
            execl("/usr/bin/ps", "ps", "-aux", NULL);
            break;
        case 1:
            execlp("ps", "ps", "-aux", NULL);
            break;
        case 2:
            execle("/usr/bin/ps", "ps", "-aux", NULL, envp);
            break;
        case 3:
            execv("/usr/bin/ps", args);
            break;
        case 4:
            execvp("ps", args);
            break;
        case 5:
            execve("/usr/bin/ps", args, envp);
            break;
        default:
            break;
    }
    printf("exec error");
}

如果进程被成功替换,那么后续的命令都不会被执行。也就是说,上面的 exec 如果成功执行,那么 printf 语句不会执行。

envp 中给出的值以键值对形式给出,并且要以 NULL 结尾。

a.sh

#!/bin/bash
# test exec
env | egrep 'aaa|bbb'

查看环境变量中的 aaa 值或 bbb 值。

a.c

char *args[] = {"bash", "./a.sh", NULL};
char *envp[] = {"aaa=132", "bbb=456", NULL};

int main(void)
{
    return execve("/bin/bash", args, envp);
}

输出
bbb=456
aaa=132

单独使用 exec 可能作用不大,因为它会将整个进程替换为另一个进程,后续的命令都不会被执行。一般通过与函数 fork() 搭配使用。

exec() & fork()

a.c

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    for (int i = 0; i < argc; ++i)
        puts(argv[i]);
}

main.c

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char *args[] = {"abc", "123", "456"};
    pid_t pid = fork();
    switch (pid) {
    case -1:
        /* error */
        perror("pid:%d\n", pid);
        return -1;
    case 0:
        /* child */
        execv("./a", args);
        return 0;
    default:
        /* parent */
        break;
    }

    printf("child pid: %d\n", pid);
    return 0;
}
gcc -o a a.c
gcc -o main main.c
./main
# display:
# abc
# 123
# 456

a 程序输出传递给 main 的参数

fork() 系统调用复制当前进程,在 Linux 的进程表中添加一个新的进程。这个新进程是原进程的子进程,它们在很多方面是一样的,也执行相同的代码,不过有自己相同的数据空间、环境和文件描述符。

pid_t 实际上就是 int,子进程的 fork() 返回 0 ,父进程的 fork() 返回子进程的 pid,如果 fork() 出错则返回 -1

fork()exec() 一起使用,先复制进程再替换进程,从而完成创建新进程的目的。

等待进程

添加头文件 sys/wait.h

wait()

pid_t wait(int *stat_loc);

a.c

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    for (int i = 0; i < argc; ++i)
        printf("%s\n", argv[i]);
    return -1;
}

main.c

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>

#include <sys/wait.h>

char *args[] = {"abc", "123", "456"};

int main(void)
{
    pid_t pid = fork();
    switch (pid) {
    case -1:
        /* error */
        printf("pid:%d\n", pid);
        return -1;
    case 0:
        /* child */
        execv("./a", args);
        return 0;
    default:
        /* parent */
        break;
    }

    int stat_val;
    pid = wait(&stat_val);

    printf("child finished - pid: %d\n", pid);

    if (WIFEXITED(stat_val))
        printf("exit code: %d\n", WEXITSTATUS(stat_val));
    else
        printf("exit abnormally");

    return 0;
}

WIFEXITED, WEXITSTATUS 分别用来检测子进程是否正常退出和获取退出码。

关键在于 pid = wait(&stat_val) 这句话上,stat_val 获取子进程退出的状态,pid 获取返回的进程,如果失败则 pid = -1

waitpid()

也可以使用 pid_t waitpid(pid_t pid, int *stat_loc, int option) 对某个特定的进程等待,由参数 pid 决定:
如果 pid > 0,匹配相同 pid 的进程;
如果 pid == -1,匹配任何子进程;
如果 pid == 0,匹配与当前进程具有相同进程组的进程;
如果 pid < -1,匹配进程组为 pid 绝对值的进程组。

option 可以改变函数的作用,其中最常用的就是 WNOHANG,它可以防止调用者挂起,也就是说只在调用这个函数的时候检查是否已经有子进程已经结束,如果没有就继续向下运行。

posted @ 2022-10-27 12:20  Violeshnv  阅读(133)  评论(0编辑  收藏  举报