一篇文章教会你什么是Linux进程控制

在这里插入图片描述

进程创建

1.fork函数初识

在Linux上一篇文章进程概念详解我们提到了在linux中fork函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
在这里插入图片描述
返回值
在这里插入图片描述

自进程中返回0,父进程返回子进程id,出错返回-1

1.1那么fork创建子进程时,操作系统都做了什么呢?

当在操作系统中调用 fork 函数创建子进程时,操作系统会执行以下一系列步骤:

复制父进程: 操作系统会创建一个新的子进程,该子进程是父进程的一个副本。子进程将会继承父进程的代码、数据、堆栈、文件描述符等信息
分配进程ID(PID): 操作系统会为新的子进程分配一个唯一的进程ID(PID)。父进程和子进程都有不同的PID。
复制文件描述符表: 子进程会复制父进程的文件描述符表。这意味着子进程可以访问与父进程相同的打开文件、网络连接等资源。
复制内存映像子进程会复制父进程的内存映像,包括代码段、数据段和堆栈。这样,子进程和父进程可以开始在不同的内存空间中执行。
创建唯一的资源: 操作系统会为子进程创建一些唯一的资源,如计时器、信号处理等。
设置返回值: 在父进程和子进程中,fork 函数会返回不同的值。在父进程中,它返回子进程的PID。在子进程中,它返回0,表示这是子进程
开始执行子进程: 子进程从 fork 函数调用的位置开始执行。这意味着子进程会执行与父进程相同的代码。

总之,fork 函数通过创建一个几乎与父进程相同的子进程,允许父进程和子进程在独立的环境中运行。这是实现多任务和多进程编程的重要机制之一。

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序:

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>                                                                                                                                                                                                               
  4 int main()
  5 {
    
    
  6     pid_t pid;
  7     printf("Before: pid is %d\n", getpid());
  8 
  9     if ( (pid=fork()) == -1 )
 10     {
    
    
 11         perror("fork()");
 12         exit(1);
 13     }
 14     printf("After:pid is %d, fork return %d\n", getpid(), pid);
 15     sleep(1);
 16     return 0;
 17 }

运行结果

[kingxzq@localhost Documents]$ ./test
Before: pid is 7052
After:pid is 7052, fork return 7053
After:pid is 7053, fork return 0

这里看到了三行输出,一行before,两行after

第7行:父进程打印了"Before: pid is 7052",表示它的进程ID(PID)是7052。
第14行:父进程打印了"After:pid is 7052, fork return 7053"。这意味着fork()调用成功,父进程接收到了子进程的PID,即7053。
第14行:子进程打印了"After:pid is 7053, fork return 0"。在子进程中,fork()调用返回0,表示它是子进程。

因此,该程序使用fork()创建了一个子进程,父进程和子进程都从fork()调用的位置继续执行。父进程接收到子进程的PID作为fork()的返回值,而子进程接收到0作为返回值。

那么为什么进程7053没有打印before呢?

进程7053没有打印"Before: pid is 7052"是因为在调用fork()之后,父进程和子进程是并发执行的。在父进程执行到打印"Before: pid is 7052"之后,它创建了一个子进程。子进程继承了父进程的代码和数据,包括printf语句,但是子进程的输出缓冲区是独立的

因此,当父进程执行完printf语句后,它的输出被刷新到终端,而子进程的输出缓冲区中仍然存在。当子进程执行到打印"After:pid is 7053, fork return 0"时,它的输出也被刷新到终端。

这就是为什么父进程和子进程的输出顺序可能会交错的原因。在这种情况下,父进程的输出先于子进程的输出,因此你看到的输出是"Before: pid is 7052"在"After:pid is 7053, fork return 0"之前打印的。注意,fork之后,谁先执行完全由调度器决定。

1.2 父子进程和CPU中的EIP(指令指针)之间存在一定的关系

当一个程序(进程)在执行时,CPU会通过EIP来跟踪下一条要执行的指令的内存地址。当遇到函数调用、分支语句或系统调用等情况时,CPU会根据程序的逻辑跳转到相应的地址执行。

在创建子进程时,通过fork()系统调用,操作系统会复制父进程的代码段、数据段和堆栈等信息给子进程。这意味着子进程会拥有与父进程相同的代码和数据。

在fork()调用之后,父进程和子进程会在不同的内存空间中独立执行。它们各自拥有自己的EIP,用于跟踪各自的执行状态。父进程和子进程的EIP会根据各自的代码逻辑独立地进行跳转和执行。

因此,父进程和子进程的EIP是相互独立的,它们在执行过程中不会相互影响。每个进程都有自己的EIP,用于指示下一条要执行的指令的地址。所以这也就是为什么子进程只会执行fork函数之后位置的代码。

1.3 fork的常规用法有哪些?

创建子进程:最常见的用法是使用fork()创建一个子进程。父进程调用fork()后,会创建一个与父进程几乎完全相同的子进程。子进程从fork()调用的位置开始执行,而父进程继续执行后续的代码。
并行处理:通过fork()可以实现并行处理任务。父进程可以将任务分配给多个子进程,每个子进程独立执行任务,从而实现并行处理,提高程序的执行效率。
进程间通信:通过fork()创建的子进程可以用于进程间通信。父进程和子进程可以通过管道、共享内存、消息队列等机制进行通信,实现数据的交换和共享。
守护进程:守护进程是在后台运行的长期运行的进程,通常通过fork()创建。父进程可以通过fork()创建一个子进程,并在子进程中执行守护进程的任务,而父进程则可以继续执行其他任务或退出。
多进程编程:fork()可以用于多进程编程,例如使用多个子进程同时处理不同的任务,或者使用子进程执行特定的功能,从而实现更复杂的程序逻辑。

这些是fork()的一些常规用法,但并不限于此。fork()是进程创建和管理的基础,可以根据具体的需求和场景进行灵活的应用。

1.4 fork调用失败的原因有哪些?

fork()调用可能会失败,导致返回-1。以下是一些可能导致fork()调用失败的原因:

系统资源不足:当系统中的进程数量已经达到了操作系统的限制时,fork()调用可能会失败。这可能是由于系统内存不足、进程数量达到上限或者其他资源限制导致的。
进程数量限制:操作系统可能对每个用户或每个进程组设置了最大进程数量的限制。当达到这个限制时,fork()调用可能会失败。
虚拟内存不足:当系统的虚拟内存空间不足以容纳新的进程时,fork()调用可能会失败。
权限不足:如果当前进程没有足够的权限来创建新的进程,例如缺少适当的权限或者超过了进程数量限制,fork()调用也会失败。
系统错误:其他系统级错误,如内核错误或其他底层问题,也可能导致fork()调用失败。

在fork()调用失败时,通常会使用perror()函数打印错误信息,并根据具体的错误原因采取适当的处理措施。

2.写时拷贝

2.1 什么是写实拷贝?

写时拷贝(Copy-on-Write,COW)是一种内存管理技术,用于在创建子进程时延迟复制父进程的内存内容。在写时拷贝中,当父进程创建子进程时,子进程会与父进程共享相同的物理内存页。

在写时拷贝的情况下,当父进程或子进程尝试修改共享的内存页时,操作系统会执行实际的内存复制操作。这样,父进程和子进程就会拥有各自的独立内存副本,而不会相互干扰。

写时拷贝的主要优势在于节省内存和提高性能。在创建子进程时,不需要立即复制整个父进程的内存空间,而是共享相同的物理内存页。这样可以减少内存的使用量,并且在父进程和子进程之间切换时,不需要进行大量的内存复制操作,提高了性能。

总结来说,写时拷贝是一种延迟复制的技术,用于在创建子进程时共享父进程的内存,只有在需要修改共享内存时才进行实际的复制操作,以提高内存利用率和性能

举个简单的例子:

在C语言中,常量字符串是指在代码中直接使用的字符串字面量,例如:“Hello, World!”。常量字符串在编译时就会被存储在程序的只读数据段(常量区)中,而不是在堆栈或堆中
类似于写时拷贝,在进程调度中,当创建一个新的进程时,操作系统通常会延迟复制父进程的内存内容。这意味着父进程和子进程会共享相同的物理内存页,包括常量字符串所在的只读数据段。
这种共享常量字符串的方式类似于写时拷贝的思想。当父进程或子进程尝试修改共享的常量字符串时,操作系统会执行实际的复制操作,将被修改的字符串复制到新的内存页中,以确保父进程和子进程拥有各自的独立副本。
这种共享常量字符串的方式可以节省内存空间,因为不需要为每个进程复制相同的字符串副本。只有在需要修改字符串时,才会进行实际的复制操作,以确保进程间的独立性。

因此,类似于写时拷贝,进程调度中的共享常量字符串的方式延迟了复制操作,提高了内存利用率,并在需要修改时才进行实际的复制,以确保进程间的独立性。

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:
在这里插入图片描述

进程终止

1.进程退出场景有哪些?

进程可以在多种场景下退出。以下是一些常见的进程退出场景:

正常退出:进程完成了它的任务,并通过调用exit()系统调用或从main()函数中返回来正常退出。在退出之前,进程可以释放资源、保存状态或执行其他必要的清理操作。
异常退出:进程在执行过程中遇到了错误或异常情况,无法继续执行下去。这可能是由于内存访问错误、除零错误、无效指令、段错误等导致的。在这种情况下,操作系统会终止进程并生成相应的错误报告。
信号终止:进程可以通过接收到特定的信号而终止。例如,当进程接收到SIGTERM信号时,它可以选择优雅地终止并执行清理操作。另外,一些信号如SIGKILLSIGSTOP是无法被捕获或忽略的,它们会立即终止进程。
父进程终止:当一个进程的父进程终止时,操作系统会将该进程的父进程设置为init进程(通常是进程ID为1的进程)。如果该进程没有被其他进程接管,它可能会成为孤儿进程,并由操作系统接管并终止。
资源耗尽:进程可能因为系统资源的耗尽而被迫终止。例如,当进程请求的内存超过系统可用内存时,操作系统可能会终止该进程以保护系统的稳定性。
被其他进程终止:其他进程可以通过发送特定的信号(如SIGKILL)来终止目标进程。这通常是由于需要强制终止进程或出于系统管理的目的。

这些是一些常见的进程退出场景,但并不限于此。进程退出的原因可以是多样的,具体取决于进程的任务、运行环境和外部因素。

2.常见查看进程退出方法

2.1 正常终止

可以通过 echo $? 查看进程退出码

在这里插入图片描述

echo $?命令用于显示上一个执行的命令的退出状态码(或称为返回值)。在Unix/Linux系统中,每个命令在执行完毕后都会返回一个退出状态码,用于表示命令执行的结果。

$?是一个特殊的变量,用于存储上一个命令的退出状态码。通过在命令行中执行echo $?,可以打印出上一个命令的退出状态码。

退出状态码通常是一个整数值,其中0表示命令成功执行,而非零值表示命令执行失败或出现错误。具体的退出状态码的含义可以根据不同的命令而有所不同,通常会在命令的文档或手册中进行说明。

echo $?命令对于调试和脚本编写非常有用,可以根据上一个命令的退出状态码来进行条件判断或错误处理。

2.2 异常退出

Ctrl+C,信号终止

你在终端中按下Ctrl+C组合键时,会发送一个SIGINT信号给当前正在运行的进程。这个信号通常用于请求进程终止。

当进程接收到SIGINT信号时,默认的行为是终止进程并进行清理操作。这被称为信号终止。进程可以选择捕获和处理SIGINT信号,例如执行一些清理操作后再终止。

在终端中按下Ctrl+C时,操作系统会将SIGINT信号发送给前台运行的进程组中的所有进程。通常情况下,这会导致当前正在运行的进程终止。

需要注意的是,有些进程可能会忽略SIGINT信号或者通过编写信号处理程序来自定义处理方式。但是,大多数情况下,按下Ctrl+C会导致进程异常退出。

2.3 _exit函数和exit函数退出

_exit()函数和exit()函数都用于终止进程,但它们之间有一些区别。

_exit()函数:

  1. _exit()函数是一个系统调用,用于立即终止进程的执行。
  2. 它不会执行任何清理操作,包括不会刷新缓冲区、关闭文件描述符等。
  3. _exit()函数的原型为void _exit(int status),其中status参数是进程的退出状态码。
  4. 进程的退出状态码可以通过父进程的wait()waitpid()系统调用来获取。

说明:虽然status是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端执行$?发现返回值是255

在这里插入图片描述
exit()函数:

  1. exit()函数是一个库函数,用于正常终止进程的执行。
  2. 在调用exit()函数之前,会执行一些清理操作,例如刷新缓冲区、关闭文件描述符等。
  3. exit()函数的原型为void exit(int status),其中status参数是进程的退出状态码。
  4. 进程的退出状态码可以通过父进程的wait()waitpid()系统调用来获取。

在这里插入图片描述
exit最后也会调用_exit, 但在调用exit之前,还做了其他工作:

  1. 执行用户通过 atexiton_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入
  3. 调用_exit

总结

  1. _exit()函数是一个系统调用,立即终止进程的执行,不执行清理操作。
  2. exit()函数是一个库函数,正常终止进程的执行,执行清理操作后退出。
  3. 两者都接受一个退出状态码作为参数,用于表示进程的退出状态。
  4. 进程的退出状态码可以通过父进程的wait()waitpid()系统调用来获取。
  5. exit也是通过调用_exit来实现的

2.4 return退出

return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做 exit的参数。

在C语言中,main函数的返回值类型通常是int类型。根据C语言标准,main函数的返回值可以是0或者非零的整数返回0表示程序成功地执行完毕,而非零的返回值通常用于表示程序执行过程中的错误或异常情况

非零的返回值可以用于向调用程序或操作系统报告错误信息或状态。例如,当程序需要在执行过程中发生错误时,可以返回一个非零值来指示错误的类型或代码。这样,调用程序或操作系统可以根据返回值来采取相应的措施,比如输出错误信息、终止程序或进行其他处理。

在实际应用中,非零的返回值可以根据具体需求进行定义和使用。不同的程序可能会定义不同的非零返回值来表示不同的错误或状态。一般来说,返回值的具体含义和用途是由程序员根据程序的逻辑和需求来决定的。

需要注意的是,main函数的返回值只能是整数类型,不能返回其他类型的值。如果需要返回其他类型的值,可以通过全局变量、指针参数或其他方式来实现

进程等待

1.什么是进程等待?

进程等待是指一个进程在执行过程中暂停自己的执行,等待某个特定的条件满足后再继续执行。进程等待的必要性主要体现在以下几个方面:

同步操作:在多进程或多线程的环境中,进程之间可能需要进行协调和同步。例如,一个进程可能需要等待其他进程完成某个任务后才能继续执行,或者需要等待某个共享资源的释放。进程等待可以确保进程之间的操作按照正确的顺序进行,避免数据竞争和不一致的结果。
资源管理:进程等待还可以用于管理系统资源的分配和释放。当一个进程需要使用某个资源时,如果该资源已经被其他进程占用,那么该进程可以选择等待资源的释放,而不是一直占用CPU资源进行忙等待。这样可以提高系统的资源利用率和效率。
阻塞操作:有些操作需要等待一段时间才能完成,例如网络通信、文件读写等。在这种情况下,进程可以选择等待操作完成后再继续执行,而不是一直占用CPU资源进行忙等待。这样可以避免资源的浪费,提高系统的响应速度。

总之,进程等待是一种有效的管理和调度进程的机制,可以确保进程之间的协调和同步,提高系统的资源利用率和效率,以及提供更好的用户体验。

2.进程等待必要性

之前博客写过,子进程退出,父进程如果不管不顾,就可能造成僵尸进程的问题,进而造成内存泄漏。另外,进程一旦变成僵尸状态,那就刀枪不入,kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息

3.进程等待的方法

3.1 wait()和waitpid()

在这里插入图片描述
wait()
pid_t wait(int*status);

返回值:
成功返回被等待进程pid,失败返回-1。

参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

waitpid()
pid_ t waitpid(pid_t pid, int *status, int options);

返回值:
当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;

参数:
pid:

如果pid等于(pid_t)-1,则请求任何子进程的状态。在这方面,waitpid()等同于wait()。
如果pid大于0,则指定要请求状态的单个子进程的进程ID。
如果pid为0,则请求任何进程组ID与调用进程相同的子进程的状态。
如果pid小于(pid_t)-1,则请求任何进程组ID等于pid的绝对值的子进程的状态。

在这里插入图片描述

status:

WIFEXITED(stat_val)如果状态是由正常终止的子进程返回的,则评估为非零值。
WEXITSTATUS(stat_val)如果WIFEXITED(stat_val)的值非零,则该宏评估为子进程传递给_exit()或exit()的状态参数的低8位,或者子进程从main()返回的值。
WIFSIGNALED(stat_val):如果状态是由未被捕获的信号终止的子进程返回的,则评估为非零值(参见<signal.h>)。
WTERMSIG(stat_val):如果WIFSIGNALED(stat_val)的值非零,则该宏评估为导致子进程终止的信号编号。
WIFSTOPPED(stat_val):如果状态是由当前停止的子进程返回的,则评估为非零值。
WSTOPSIG(stat_val):如果WIFSTOPPED(stat_val)的值非零,则该宏评估为导致子进程停止的信号编号。
WIFCONTINUED(stat_val):如果状态是由从作业控制停止中继续的子进程返回的,则评估为非零值。

在这里插入图片描述

options:

WCONTINUED:waitpid()函数将报告由pid指定的任何继续运行的子进程的状态,只要该子进程自从作业控制停止后其状态尚未被报告。
WNOHANG:如果status对于由pid指定的任何子进程不立即可用,waitpid()函数将不会挂起调用线程的执行(父进程非阻塞等待)。
WUNTRACED:任何由pid指定的已停止的子进程的状态,且自从它们停止后其状态尚未被报告,也将被报告给请求进程。

在这里插入图片描述

如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
如果不存在该子进程,则立即出错返回。

3.2 获取子进程status

wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
如果传递NULL,表示不关心子进程的退出状态信息。
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16bit)
在这里插入图片描述
结合这张图片我们可以知晓可以用于从状态字中提取信号编号和退出码。

status & 0x7F:这个表达式使用了位运算与操作符(&)和一个掩码(0x7F)。掩码0x7F的二进制表示为01111111,它的作用是将状态字中的高位清零,只保留最低的7位。这样做的目的是提取信号编号,因为信号编号通常存储在状态字的最低位。

(status >> 8) & 0xFF:这个表达式使用了右移操作符(>>)和位运算与操作符(&),以及一个掩码(0xFF)。首先,status >> 8将状态字向右移动8位,将退出码移动到最低位。然后,位运算与操作符&与掩码0xFF进行与操作,将高位清零,只保留最低的8位。这样做的目的是提取退出码,因为退出码通常存储在状态字的高8位。

综上所述,这种方式通过使用位运算和掩码,从状态字中提取信号编号和退出码。

3.3 进程等待示例

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>

int code = 0; // 定义一个全局变量code,用于存储子进程的退出码

int main()
{
    
    
    pid_t id = fork(); // 创建一个子进程
    if(id < 0)
    {
    
    
        perror("fork"); // 如果创建子进程失败,则输出错误信息
        exit(1); // 退出程序,返回状态码1
    }
    else if(id == 0)
    {
    
    
        // 子进程
        int cnt = 5; // 定义一个计数器
        while(cnt)
        {
    
    
            printf("cnt: %d, 我是子进程, pid: %d, ppid : %d\n", cnt, getpid(), getppid()); // 打印子进程的信息
            cnt--;
            sleep(1); // 子进程休眠1秒
        }
        code = 15; // 将全局变量code的值设置为15
        exit(15); // 子进程退出,返回退出码15
    }
    else
    {
    
    
        // 父进程
        printf("我是父进程, pid: %d, ppid: %d\n", getpid(), getppid()); // 打印父进程的信息
    
        int status = 0; // 定义一个变量用于存储子进程的状态
        pid_t ret = waitpid(id, &status, 0); // 阻塞式的等待子进程退出
        if(ret > 0)
        {
    
    
            printf("等待子进程成功, ret: %d, 子进程收到的信号编号: %d, 子进程退出码: %d\n",\
                    ret, status & 0x7F ,(status >> 8) & 0xFF); // 打印子进程的退出信息
            printf("code: %d\n", code); // 打印全局变量code的值
        }
       
    }
}

这段代码创建了一个父进程和一个子进程,父进程通过fork()函数创建子进程。子进程会打印自己的信息,并在循环中每秒减少计数器的值,直到计数器为0。然后,子进程将全局变量code的值设置为15,并退出。父进程会打印自己的信息,并使用waitpid()函数阻塞等待子进程退出。当子进程退出后,父进程会打印子进程的退出信息,包括子进程收到的信号编号和退出码,以及全局变量code的值。

在这里插入图片描述
第一次运行让其正常终止,所以没有信号编号的返回,正常走exit函数退出,退出码为15,即为我们自己定义的退出码。
第二次运行期间我们使用kill -9 子进程pid 命令终止子进程,信号编号返回9,此时的退出码并无意义,因为程序非正常退出。

还需要注意的是,这里不管以何种方式终止进程,全局变量code始终为0,这是因为子进程和父进程是两个独立的进程,它们有各自独立的内存空间。在子进程中修改全局变量 code 的值,不会影响父进程中的 code 变量。子进程的修改只影响子进程内部的变量,而不会影响父进程的变量。

要实现子进程修改全局变量并使其对父进程可见,可以使用进程间通信机制,例如管道(Pipe)或共享内存(Shared Memory)。这样父子进程之间可以共享一块内存区域,使得修改在两个进程中都可见。

常见的信号编号如下(这里只做了解):

SIGHUP (1): 终端挂起或控制进程终止。
SIGINT (2): 中断信号,通常是Ctrl+C。
SIGQUIT (3): 退出信号,通常是Ctrl+\,会产生核心转储。
SIGILL (4): 非法指令。
SIGABRT (6): 终止信号,通常是abort()函数发出的信号。
SIGFPE (8): 浮点异常。
SIGKILL (9): 强制终止,不能被忽略、阻塞或捕获。
SIGSEGV (11): 段错误,试图访问无法访问的内存。
SIGPIPE (13): 管道破裂。
SIGALRM (14): 定时器超时。
SIGTERM (15): 终止信号,常用于请求进程正常终止。
SIGUSR1 (10): 用户自定义信号1。
SIGUSR2 (12): 用户自定义信号2。
SIGCHLD (17): 子进程状态改变,例如子进程终止时发送给父进程。
SIGCONT (18): 继续执行一个已停止的进程。
SIGSTOP (19): 停止信号,用于停止进程的执行。
SIGTSTP (20): 终端停止信号,通常是Ctrl+Z。
SIGTTIN (21): 后台进程试图从控制终端读取。
SIGTTOU (22): 后台进程试图向控制终端写入。
SIGBUS (7): 总线错误,试图访问不属于你的内存地址。

3.4 进程的阻塞等待方式

  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 #include <sys/wait.h>
  5 int main()
  6 {
    
    
  7         pid_t id = fork();
  8         if(id == 0)
  9         {
    
    
 10             //子进程
 11             printf("子进程开始运行, pid: %d\n", getpid());
 12             sleep(3);
 13         }
 14         else
 15         {
    
    
 16             //父进程
 17             printf("父进程开始运行, pid: %d\n", getpid());
 18             int status = 0;                                                                                                                                                                                                      
 19             pid_t id = waitpid(-1, &status, 0); //阻塞等待, 一定是子进程先运行完毕,然后父进程获取之后,才退出!
 20             if(id > 0)
 21             {
    
    
 22                 printf("wait success, exit code: %d\n", WEXITSTATUS(status));
 23             }
 24         }
 25     return 0;
 26 }

运行结果

[kingxzq@localhost Documents]$ ./test1
父进程开始运行, pid: 12554
子进程开始运行, pid: 12555
wait success, exit code: 0

3.5 进程的非阻塞等待方式

  1 #include <stdio.h>
  2 #include <unistd.h>
  3 #include <stdlib.h>
  4 #include <sys/wait.h>
  5 int main()
  6 {
    
    
  7     pid_t pid;
  8     pid = fork();
  9     if(pid < 0){
    
    
 10         printf("%s fork error\n",__FUNCTION__);
 11         return 1;
 12     }
 13     else if( pid == 0 ){
    
     //child
 14         printf("child is run, pid is : %d\n",getpid());
 15         sleep(5);
 16         exit(1);
 17     }   
 18     else{
    
    
 19         int status = 0;
 20         pid_t ret = 0;
 21         do
 22         {
    
    
 23             ret = waitpid(-1, &status, WNOHANG);//非阻塞式等待
 24             if( ret == 0 ){
    
    
 25                 printf("child is running\n");
 26             }
 27             sleep(1);
 28         }while(ret == 0);
 29         if( WIFEXITED(status) && ret == pid ){
    
    
 30             printf("wait child 5s success, child return code is :%d.\n",WEXITSTATUS(status));
 31         }
 32         else{
    
    
 33             printf("wait child failed, return.\n");
 34             return 1;
 35         }
 36     }
 37     return 0;
 38 } 

运行结果

[kingxzq@localhost Documents]$ ./test
child is running
child is run, pid is : 13231
child is running
child is running
child is running
child is running
wait child 5s success, child return code is :1.

kill命令终止运行结果

[kingxzq@localhost Documents]$ ./test
child is running
child is run, pid is : 13268
child is running
child is running
child is running
wait child failed, return.

3.6 进程的阻塞等待方式和进程的非阻塞等待方式有什么区别

进程的阻塞等待方式和进程的非阻塞等待方式是两种不同的等待子进程状态变化的方式:

阻塞等待:当父进程调用等待函数(如wait、waitpid等)等待子进程退出时,父进程会一直阻塞(即挂起自己的执行),直到子进程退出或发生其他指定的状态变化。在等待期间,父进程不会继续执行其他任务。
非阻塞等待:当父进程调用非阻塞等待函数(如waitpid函数的使用WNOHANG标志)等待子进程退出时,父进程会继续执行自己的任务,不会被阻塞。父进程会立即返回等待函数,无论子进程的状态是否发生变化。非阻塞等待允许父进程在等待子进程的同时继续执行其他任务。

总之,阻塞等待会导致父进程在等待子进程状态变化期间被挂起,而非阻塞等待允许父进程在等待子进程的同时继续执行其他任务。选择使用哪种等待方式取决于具体的应用场景和需求。

进程程序替换

1.替换原理

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
在这里插入图片描述

2.替换函数

在这里插入图片描述
这些函数是用于在Linux/Unix操作系统中执行新的程序的系统调用函数。它们的作用是在一个进程内启动一个新的程序执行,取代当前进程的执行。这些函数在C语言标准库头文件<unistd.h>中声明。

下面是对这些函数的简要说明:

execl:

原型int execl(const char *path, const char *arg, ...);
功能:用于执行指定路径的可执行文件,第一个参数是要执行的程序的路径,后面的参数是传递给新程序的命令行参数,以NULL为结束标志。
示例execl("/bin/ls", "ls", "-l", NULL);

execlp:

原型int execlp(const char *file, const char *arg, ...);
功能:类似于execl,但是它会在系统的路径中搜索可执行文件。
示例execlp("ls", "ls", "-l", NULL);

execle:

原型int execle(const char *path, const char *arg, ..., char *const envp[]);
功能:类似于execl,但是可以指定新程序的环境变量。最后一个参数是一个指向环境变量的指针数组,以NULL为结束标志。
示例char *const envp[] = {"PATH=/bin", NULL}; execle("/bin/ls", "ls", "-l", NULL, envp);

execv:

原型int execv(const char *path, char *const argv[]);
功能:类似于execl,但是参数传递方式是使用一个指向参数字符串数组的指针。第一个参数是要执行的程序的路径,第二个参数是指向参数字符串数组的指针,以NULL为结束标志。
示例char *const argv[] = {"ls", "-l", NULL}; execv("/bin/ls", argv);

execvp:

原型int execvp(const char *file, char *const argv[]);
功能:类似于execv,但是它会在系统的路径中搜索可执行文件。
示例char *const argv[] = {"ls", "-l", NULL}; execvp("ls", argv);

execvpe:

原型int execvpe(const char *path, char *const argv[], char *const envp[])
功能:类似于execv,但是可以指定新程序的环境变量,最后一个参数是一个指向环境变量的指针数组,以NULL为结束标志。
示例char *const argv[] = {"ls", "-l", NULL}; char *const envp[] = {"PATH=/bin", NULL}; execve("/bin/ls", argv, envp);

这些函数通常用于在一个进程内部启动一个新的程序,新程序取代当前进程的执行。执行成功则不会返回,失败则会返回-1并设置errno。它们通常用于在C程序中执行其他程序,比如在Shell中运行命令。

看下面的两段代码:
mycmd.c

  1 #include <stdio.h>
  2 #include <string.h>
  3 #include <stdlib.h>
  4 
  5 int main(int argc, char *argv[])
  6 {
    
    
  7     if(argc != 2)
  8     {
    
    
  9         printf("can not execute!\n");
 10         exit(1);
 11     }
 12 
 13     printf("获取环境变量: MY_VAL: %s\n", getenv("MY_VAL"));                                                 
 14 
 15     if(strcmp(argv[1], "-a") == 0)
 16     {
    
    
 17         printf("hello a!\n");
 18     }
 19     else if(strcmp(argv[1], "-b") == 0)
 20     {
    
    
 21         printf("hello b!\n");
 22     }
 23     else{
    
    
 24         printf("default!\n");
 25     }
 26 
 27     return 0;
 28 }

test.c

    1 #include <stdio.h>
    2 #include <stdlib.h>
    3 #include <unistd.h>
    4 #include <sys/wait.h>
    5 
    6 #define NUM 16
    7 
    8 const char *myfile = "./mycmd";
    9 
   10 int main(int argc, char*argv[], char *env[])
   11 {
    
    
   12        char *const _env[NUM] = {
    
    
   13            (char *)"MY_VAL=23333333",
   14            NULL
   15        };
   16             printf("进程开始运行, pid: %d\n", getpid());        
   17             sleep(3);                                           
   18             char *const _argv[NUM] = {
    
                              
   19                 (char*)"ls",                                    
   20                 (char*)"-a",                                    
   21                 (char*)"-l",                                    
   22                 (char*)"-i",                                    
   23                 NULL                                            
   24             };                                                  
   25                                                                 
   26             //execl(myfile, "mycmd", "-b", NULL);//调用自己的进程   
   27             //execl("/usr/bin/ls", "ls", "-a", "-l", NULL);//调用系统ls进程,自己输入字符命令
   28             //execlp("./test.py", "test.py", NULL);//调用自建的python进程   
   29             //execlp("python", "python", "test.py", NULL);//结果同上,调用形式不同
   30             //execlp("bash", "bash", "test.sh", NULL); //调用自建的shell进程
   31             //execlp("ls", "ls", "-a", "-l", NULL); //调用系统ls进程,自己输入字符命令
   32             execle(myfile, "mycmd", "-a", NULL, _env);          
   33                                                                 
   34             //execv("/usr/bin/ls", _argv); //和上面的execl只有传参方式的区别
   35             //execvp("ls", _argv);//调用系统ls进程,输入字符串数组名
   36             //execvpe("/usr/bin/ls",_argv,env);//效果同execv,多了一个环境变量参数                        
   37     return 0;                                                               
   38 }  

运行结果:

[kingxzq@localhost Documents]$ ./test
进程开始运行, pid: 18076
获取环境变量: MY_VAL: 23333333
hello a!

3.函数解释

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
如果调用出错则返回-1
所以exec函数只有出错的返回值而没有成功的返回值。

4.命名理解

这些函数原型看起来很容易混,但只要掌握了规律就很好记

l(list) : 表示参数采用列表
v(vector) : 参数用数组
p(path) : 有p自动搜索环境变量PATH
e(env) : 表示自己维护环境变量

在这里插入图片描述
下图exec函数族 一个完整的例子:
在这里插入图片描述

制作简易shell

就像系统中的bash(即shell),完成下面这类操作

在这里插入图片描述
用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。
在这里插入图片描述
然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。
所以要写一个shell,需要循环以下过程:

1. 获取命令行
2. 解析命令行
3. 建立一个子进程(fork)
4. 替换子进程(execvp)
5. 父进程等待子进程退出(wait)

代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>

#define NUM 1024
#define SIZE 32
#define SEP " "

//保存完整的命令行字符串
char cmd_line[NUM];

//保存打散之后的命令行字符串
char *g_argv[SIZE];

//环境变量的buffer
char g_myval[64];

// shell 运行原理 : 通过让子进程执行命令,父进程等待&&解析命令
int main()
{
    
    
    extern char**environ;//获取全局环境变量的指针
    //0. 命令行解释器,一定是一个常驻内存的进程,不退出
    while(1)
    {
    
    
        //1. 打印出提示信息 [kingxzq@localhost myshell]# 
        printf("[kingxzq@localhost myshell]# ");
        fflush(stdout);
        memset(cmd_line, '\0', sizeof cmd_line);
        //2. 获取用户的键盘输入[输入的是各种指令和选项: "ls -a -l -i"]
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
        {
    
    
            continue;
        }
        if (strlen(cmd_line) <= 1) {
    
     // 如果只有回车换行符,长度为1
            continue;//输入为空重新输入
        }
        cmd_line[strlen(cmd_line)-1] = '\0';//用\0将\n替换
        //"ls -a -l -i\n\0"
        //3. 命令行字符串解析:"ls -a -l -i" -> "ls" "-a" "-i"
        g_argv[0] = strtok(cmd_line, SEP); //第一次调用,要传入原始字符串
        int index = 1;
        if(strcmp(g_argv[0], "ls") == 0)
        {
    
    
            g_argv[index++] = "--color=auto";//添加自动颜色
        }
        if(strcmp(g_argv[0], "ll") == 0)//ll本身为ls -l,所以单独添加一个命令
        {
    
    
            g_argv[0] = "ls";
            g_argv[index++] = "-l";
            g_argv[index++] = "--color=auto";
        }
        while(g_argv[index++] = strtok(NULL, SEP)); //第二次,如果还要解析原始字符串,传入NULL
        if(strcmp(g_argv[0], "export") == 0 && g_argv[1] != NULL)//添加环境变量
        {
    
    
            strcpy(g_myval, g_argv[1]);
            int ret = putenv(g_myval);//输入环境变量
            if(ret == 0) printf("%s export success\n", g_argv[1]);
                continue;
        }

        //4.内置命令, 让父进程(shell)自己执行的命令,我们叫做内置命令,内建命令
        //内建命令本质其实就是shell中的一个函数调用
        if(strcmp(g_argv[0], "cd") == 0) //cd命令调用
        {
    
    
            if(g_argv[1] != NULL) chdir(g_argv[1]); //cd path, cd ..

            continue;
        }
        //5. fork()
        pid_t id = fork();
        if(id == 0) //child
        {
    
    
            printf("下面功能让子进程进行的\n");
            printf("child, MYVAL: %s\n", getenv("MYVAL"));//获取我们输入的环境变量MYVAL
            printf("child, PATH: %s\n", getenv("PATH"));//获取环境变量路径
            execvp(g_argv[0], g_argv); // ls -a -l -i
            exit(1);
        }
        //father
        int status = 0;
        pid_t ret = waitpid(id, &status, 0);
        if(ret > 0) printf("exit code: %d\n", WEXITSTATUS(status));//退出码接收
    }
    return 0;
}

测试ls命令
在这里插入图片描述
测试添加环境变量和cd命令

在这里插入图片描述

结语

有兴趣的小伙伴可以关注作者,如果觉得内容不错,请给个一键三连吧,蟹蟹你哟!!!
制作不易,如有不正之处敬请指出
感谢大家的来访,UU们的观看是我坚持下去的动力
在时间的催化剂下,让我们彼此都成为更优秀的人吧!!!

猜你喜欢

转载自blog.csdn.net/kingxzq/article/details/132159002