当前位置: 首页 > news >正文

【Linux】进程管理(2):进程控制

一、进程创建:fork函数

我们在命令行中输入man fork 即可得到fork函数的函数接口的函数的使用方法。

 我们可以看到,fork函数位于man手册的第2部分,由于第2部分通常是用于描述系统调用和库函数,所以我们可以了解到fork函数实际是一个系统调用函数。

接下来,我们先了解一下什么是系统调用函数?

系统调用函数是操作系统提供给用户程序或应用程序的一组接口,通过这些接口,用户程序可以请求操作系统执行特定的操作,如文件操作、进程管理、网络通信等。系统调用函数允许用户程序访问操作系统的底层功能,以完成对硬件资源的管理和控制。

系统调用函数与一般的函数调用有所不同。一般的函数调用是在用户程序内部进行的,而系统调用函数是用户程序与操作系统之间的通信方式。当用户程序调用系统调用函数时,会触发一个特殊的处理机制,将控制权转移给操作系统内核,执行相应的操作,然后将结果返回给用户程序。

系统调用函数通常是由操作系统提供的库函数封装的,以便用户程序更方便地调用。这些函数通常包含在标准库中,例如在 C 语言中,可以通过 unistd.h 头文件来访问系统调用函数。

常见的系统调用函数包括 fork()exec()open()read()write() 等,它们提供了对文件系统、进程管理、内存管理、网络通信等底层功能的访问。系统调用函数是编写操作系统相关程序和系统编程的重要工具,也是操作系统与用户程序之间的桥梁。

如果不理解,我们先记住加粗蓝字描述的部分。 

在操作系统中,用户程序处于用户态(用户层),而操作系统内核处于内核态(核心层)。用户程序不能直接访问系统的硬件资源或执行特权指令,而是通过系统调用接口来请求操作系统执行特定的任务,包括对硬件资源的管理和控制。

通过系统调用接口,用户程序可以向操作系统发出请求,比如读写文件、创建进程、进行网络通信等。操作系统会根据请求执行相应的操作,然后将结果返回给用户程序。这样的设计有效地保护了系统的稳定性和安全性,同时也提供了一种方便而有效的方式,让用户程序与系统进行交互。

 fork函数详解:

fork函数从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

  • 接口

    #include <unistd.h>
    pid_t fork(void);
    
  • 作用
    fork() 函数用于创建一个新的进程,该进程是调用进程的副本。子进程与父进程几乎完全相同,包括代码段、数据段、堆栈等。在子进程中,fork() 返回 0,而在父进程中,它返回新创建子进程的 PID(进程标识符)。

  • 返回值

    • 在父进程中,fork() 返回新创建子进程的 PID。
    • 在子进程中,fork() 返回 0。
    • 如果 fork() 失败,返回值为 -1,表示创建子进程失败。
  • 进程的执行

    • 子进程从 fork() 返回的地方【return】开始执行,而父进程则继续执行它的代码。这意味着在 fork() 调用之后,父进程和子进程会并行执行。
  • 错误处理
    如果 fork() 失败,返回值为 -1。失败的原因可能是系统资源不足或者进程数达到了限制。

  • 注意事项

    • 在 fork() 后,父子进程共享文件描述符,这意味着在一个进程中打开的文件在另一个进程中也是打开的。如果不适当地处理,可能会导致意想不到的结果。
    • 子进程通常需要调用 exec 系列函数来加载新的程序,以便替换掉自己的内存映像。否则,子进程将继承父进程的内存映像,可能会导致一些意外的行为。

 接下来我们来看一段程序:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{printf("父进程开始运行!!!\n");pid_t id = fork();if(id == 0){printf("我是子进程!!!\n");sleep(1);}else if(id > 0){printf("我是父进程!!!\n");sleep(1);}else {perror("进程创建失败!!!\n");}return 0;
}

 这段代码的结果是这样的:

 接下来我们来详细聊一下fork函数。相信大家都有这样的疑问:

1、为什么fork()函数可以有两个返回值,也就是函数会返回两次,这和我们平时见到的函数不同。

当进程调用 fork() 函数时,控制会转移到操作系统内核中执行 fork() 函数的代码。在内核中,fork() 函数主要完成以下操作:

  1. 创建新的进程控制块(Process Control Block,PCB):内核会为新的子进程分配一个唯一的进程标识符(PID),并在内存中为其创建一个新的进程控制块(PCB)。这个 PCB 将包含子进程的运行状态、程序计数器、堆栈指针、文件描述符等信息。

  2. 复制父进程的地址空间以创建自己的地址空间:在大多数情况下,fork() 函数会创建子进程的完整副本,包括代码段、数据段、堆栈等。这意味着子进程将会获得与父进程几乎完全相同的内存映像。这一步通常通过 Copy-On-Write(写时复制)技术来实现,即在子进程需要修改内存时才会进行实际的复制操作。

  3. 将子进程的状态设置为就绪:一旦子进程的地址空间准备好,内核将其状态设置为就绪态,以便在合适的时机可以被调度执行。

  4. 返回不同的值:在内核中,fork() 函数会返回两次,一次是在父进程的上下文中返回子进程的 PID,另一次是在子进程的上下文中返回 0。这样,父进程和子进程可以根据返回值来执行不同的代码路径。

  

 在fork函数内部,在执行 return pid 之前,子进程就已经创建完成,所以 return pid 实际也是父子进程的共享代码部分,所以父进程会执行一次,返回子进程的pid;而子进程也会执行一次 return pid 返回进程是否创建完成的信息。

 2、为什么父进程接收子进程的PID,而子进程返回0或-1?

  1. 父进程接收子进程的PID:父进程在调用fork()函数后,会得到子进程的PID作为返回值。通过这个PID,父进程可以对子进程进行跟踪、管理和通信。例如,父进程可能会使用子进程的PID来等待子进程的结束状态(通过waitpid()函数),或者向子进程发送信号(通过kill()函数)等。

  2. 子进程返回0或-1:子进程在fork()函数返回时,需要确定自己是父进程还是子进程。因此,子进程通常会检查fork()的返回值来确定自己的身份。具体来说:

    • 如果fork()返回0,则表示当前进程是子进程。子进程可以通过这个返回值来区别自己和父进程,并且通常会在这个基础上执行特定的任务或代码段。
    • 如果fork()返回-1,则表示进程创建失败。通常这种情况会发生在系统资源不足或者其他错误发生时。子进程在这种情况下会立即退出或者采取相应的错误处理措施。

由于fork()函数具有以上两个特性,我们可以采取 if---else 语句对父子进程进行分流,这样就可以让父子进程去做不同的事情,这也是我们后续进行进程替换的基础。

3、父子进程哪个先运行? 

在一般情况下,无法确定父进程和子进程哪一个先运行。这取决于操作系统的调度策略以及各种竞争条件的发生情况。

通过上面的知识,我们了解到在fork函数内部子进程创建完成之后,父子进程共享进程创建完成之后的代码,包括fork函数内部的代码。

二、进程终止

进程退出的场景:

  • 正常退出:进程完成了它的任务,并通过调用 exit()、_exit()函数或者在 main() 函数中使用 return 语句返回。在这种情况下,进程会执行清理操作,并返回退出状态给操作系统。

  • 异常退出:进程在执行过程中遇到了错误或异常情况,无法继续执行下去。这可能是因为代码中的错误、系统资源不足、权限不足等原因导致的。在这种情况下,进程可以调用 exit() 函数或者 _exit() 函数来立即终止程序执行,并返回退出状态给操作系统。

  • 收到信号:进程可以收到来自操作系统或其他进程发送的信号,例如 使用 kill 命令搭配SIGKILL 或 SIGTERM 等。

  • 父进程终止:如果一个子进程的父进程终止了,而没有等待子进程完成(通过调用 wait() 或 waitpid()),则子进程可能会成为孤儿进程。在这种情况下,操作系统通常会将孤儿进程的父进程设置为 init 进程(进程号为 1),并由 init 进程接管孤儿进程的管理。孤儿进程的退出方式与其他进程相同。

  • 系统关闭:当系统关闭或重启时,所有正在运行的进程都会被终止。操作系统通常会向所有进程发送信号,以便它们有机会在关闭之前执行清理操作。

  • 其他。。。

 进程退出方法:

  1. 调用 exit()、_exit() 函数

  2. 在main()函数中使用return语句返回

  3. Ctrl + c 组合键 或者 使用 kill 命令搭配终止信号

 exit()函数、_exit()函数详解

exit() 函数和 _exit() 函数都是用于终止程序的执行

  1. exit() 函数:

    • exit() 函数是标准 C 库中的函数,用于正常终止程序的执行,并返回退出状态给操作系统。
    • 调用 exit() 函数会执行以下步骤:
      1. 执行所有注册的退出处理程序(使用 atexit() 注册的函数)。
      2. 关闭所有打开的流(文件)。
      3. 刷新所有的缓冲区。
      4. 返回退出状态给操作系统。
    • exit() 函数的原型在 <stdlib.h> 头文件中声明,其函数原型如下:
      #include <stdlib.h>
      void exit(int status);
      
    • status 参数是传递给操作系统的退出状态,通常用来指示程序的结束状态,一般约定 0 表示成功,非零值表示失败或其他特定状态。
    • exit() 函数在正常终止程序时应该被调用,它会执行标准的程序清理操作,并返回状态给操作系统。
  2. _exit() 函数:

    • _exit() 函数是系统调用,用于立即终止程序的执行,不执行标准的程序清理操作。
    • 调用 _exit() 函数会立即终止程序的执行,并且不会执行以下操作:
      1. 不执行注册的退出处理程序。
      2. 不关闭打开的流(文件)。
      3. 不刷新缓冲区。
    • _exit() 函数的原型在 <unistd.h> 头文件中声明,其函数原型如下:
      #include <unistd.h>
      void _exit(int status);
      
    • status 参数同样是传递给操作系统的退出状态。
    • _exit() 函数通常在需要立即终止程序执行,并且不需要执行标准清理操作时使用。比如,在子进程中的错误处理中可以使用 _exit() 来避免执行父进程中的清理操作。

主要区别总结:

  • exit() 是标准 C 库函数,执行标准的程序清理操作后返回退出状态给操作系统。
  • _exit() 是系统调用,立即终止程序的执行,不执行标准的程序清理操作。

在使用时,通常情况下,应该优先使用 exit() 函数来正常终止程序,并执行必要的清理操作。

exit最后也会调用exit, 但在调用exit之前,还做了其他工作:

  • 执行标准的程序清理操作:调用 atexit() 注册的清理函数、关闭文件描述符等
  • 关闭所有打开的流,所有的缓存数据均被写入
  • 调用_exit

 通过上图,我们可以明显观察到三者的差异。

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

三、进程等待

在学习进程等待前,我们要先了解什么是进程等待?

进程等待(Process Waiting)是指在操作系统中,一个进程因为某种原因(通常是等待某些资源或条件满足)而被阻塞,暂时无法执行,需要等待直到满足条件才能继续执行的状态。这种状态通常发生在进程请求某种资源(如输入/输出设备、内存、锁等)而资源暂时不可用时,进程会被置于等待状态,直到资源可用或条件满足后才能继续执行。在进程等待的过程中,操作系统可以将该进程从可执行状态转换为阻塞状态,以便其他可执行的进程有机会执行。

举个简单的例子:在C语言中,我们使用printf函数向屏幕打印信息。我们让下面的程序一直向显示器打印信息,并观察该进程的状态。

#include <stdio.h>
#include <unistd.h>int main()
{while(1){printf("I am a running process, my_pid:%d\n", getpid());sleep(1);}return 0;
}

看到这儿可能有同学会产生疑惑了:进程明明是一直在运行呀,不应该是R状态(运行态)吗?为什么是S状态(睡眠态)呢?

其实这与CPU和显示器的响应速度有关。我们知道,CPU处理信息的速度是极快的,特别对于这种极其简单的程序。但当CPU每次处理完自身任务后,显示器可能仍在处理之前的信息。而在这期间后续进程需要等待显示器准备就绪,这会导致后续进程处于阻塞状态,直到显示器空闲并且操作系统将控制权交给它们。

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

wait() 和 waitpid() 函数都是用于父进程等待子进程结束的方法。它们的功能是类似的,但在使用上有些微妙的差别。

wait() 函数

wait() 函数的原型如下:

#include <sys/types.h>
#include <sys/wait.h>pid_t wait(int *status);
  • 参数 status 是一个指向整数的指针,用于存储子进程的退出状态信息,不关心可以设置成NULL。
  • 返回值是被等待子进程的进程ID(PID),如果没有子进程或出错,则返回 -1。

wait() 函数的作用是挂起当前进程,直到任何一个子进程退出为止。当子进程结束时,父进程会收到一个信号,并在收到信号后继续执行。此时,可以通过 status 参数获取子进程的退出状态信息。

waitpid() 函数

waitpid() 函数的原型如下:

#include <sys/types.h>
#include <sys/wait.h>pid_t waitpid(pid_t pid, int *status, int options);
  • pid 参数指定要等待的子进程ID。如果 pid 为 -1,则等待任意子进程;如果为 0,则等待与当前进程组ID相同的任一子进程;如果为正数,则等待指定PID的子进程;如果为负数,则等待进程组ID与 pid 绝对值相同的任一子进程。
  • status 参数同样是用于存储子进程退出状态信息的指针,不关心可以设置成NULL。
  • options 参数是一组选项,用于指定等待行为的一些额外条件,如是否非阻塞等,如不使用可以设置为0,默认阻塞等待。
  • waitpid() 函数的返回值有三种可能性:

    • 如果返回一个正值,这个值就是已经终止的子进程的进程 ID(PID)。

    • 如果返回 0,表示调用了 WNOHANG 选项,并且当前没有已终止的子进程。

    • 如果返回 -1,表示发生了错误,这时需要检查 errno 来获取具体的错误信息。

区别总结

  • wait() 函数只能等待任何子进程退出,而 waitpid() 函数允许指定具体的子进程ID。
  • waitpid() 函数还可以通过 options 参数指定一些额外的选项,如是否非阻塞等。
  • 两个函数都会挂起调用它们的进程,直到等待的子进程退出为止。

 waitpid()函数options选项:

waitpid() 函数的 options 参数用于指定等待行为的一些额外条件,包括但不限于以下几种选项:

  • WNOHANG:指定非阻塞模式。如果没有子进程退出,立即返回,不挂起父进程。
  • WUNTRACED:也会等待已经停止的子进程退出,但不会等待已经恢复执行的子进程。
  • WCONTINUED:等待已经继续执行的子进程退出。
  • WSTOPPED:等待已经停止的子进程退出,与 WUNTRACED 类似。
  • 这些选项可以单独使用,也可以通过按位或运算组合使用。例如,options 可以是 WNOHANG | WUNTRACED,表示以非阻塞模式等待子进程退出,并且同时等待已经停止的子进程退出。

  • 这些选项实际上是预先定义好的宏,在 sys/wait.h 头文件中进行了声明。当你使用这些宏时,编译器会将其替换为相应的整数值,以便在 waitpid() 函数中使用。

status参数详解

status 参数是一个输出型参数,用于由进程本身提供子进程的退出状态信息。当子进程退出时,其退出状态会被写入到 status 参数指向的内存位置中,以供父进程获取。 

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)

在status的低16比特位当中,高8位表示进程的退出状态,即退出码。进程若是被信号所杀,则低7位表示终止信号,而第8位比特位是core dump标志。 

我们通过位运算操作就可以得出程序的退出状态和终止信号:

exit_code = (status >> 8) & 0xff; //退出状态,取9~16位
exit_signal = status & 0x7f;      //终止信号,取1~7位

同时,库中也提供了两个宏来代替上面的运算:

  • WEXITSTATUS(status) 宏函数用于提取子进程的退出码,前提是该子进程是通过正常终止而退出的。其原型为:

    int WEXITSTATUS(int status);
    

    当且仅当 WIFEXITED(status) 返回真(非零值)时,才应该用 WEXITSTATUS(status)。这个宏可以帮助你获取子进程退出时传递给 exit() 函数的退出码,通常用于查看子进程的退出状态。

  • WIFEXITED(status) 宏函数用于检查子进程是否为正常终止,本质是检查是否收到信号,其原型为:

    int WIFEXITED(int status);
    

    如果子进程正常终止并成功退出,则该宏返回真(非零值),否则返回假(0)。这个宏可以帮助你确定子进程是否是通过调用 exit() 或 _exit() 等函数正常退出的。

 所以也可以如下表示:

exit_code = WEXITSTATUS(status);  //获取退出码
exit_signal = WIFEXITED(status);  //是否正常退出

使用示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{pid_t id = fork();if(id == 0){//childint count = 10;while(count--){printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());sleep(1);}exit(0);}//fatherint status = 0;pid_t ret = wait(&status);//pid_t ret = wait(-1, &status, 0); 此方法同上if(ret > 0){//wait successprintf("wait child success...\n");if(WIFEXITED(status)){//exit normalprintf("exit code:%d\n", WEXITSTATUS(status));}}sleep(1);return 0;
}

 可以看到父进程成功等待到子进程,且子进程的是正常退出。

如果我们使用kill -9 信号使子进程强制退出呢?

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
int main()
{pid_t id = fork();if(id == 0){//childint count = 20;while(count--){printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());sleep(1);}exit(0);}//fatherint status = 0;pid_t ret = waitpid(-1, &status, 0);if(ret > 0){//wait successprintf("wait child success...\n");//判断是否正常退出if(WIFEXITED(status)){//exit normalprintf("exit code:%d\n", WEXITSTATUS(status));//正常退出则打印退出码}else {//exit abnormalprintf("exit_signal:%d\n",status & 0x7f);}}sleep(3);return 0;
}

 可以看到,当使用kill -9 强制终止子进程时,父进程依然等待成功,但是由于使用的是信号,所以是异常退出,最终程序只打印出了退出信号。

进程的阻塞等待方式:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{pid_t pid;pid = fork();if (pid < 0) {printf("%s fork error\n", __FUNCTION__);return 1;}else if (pid == 0) { //childprintf("child is run, pid is : %d\n", getpid());sleep(5);exit(257);}else {int status = 0;pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5Sprintf("this is test for wait\n");if (WIFEXITED(status) && ret == pid) {printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));}else {printf("wait child failed, return.\n");return 1;}}return 0;
}

上述所给例子中,当子进程未退出时,父进程都在一直等待子进程退出,在等待期间,父进程不能做任何事情,这种等待叫做阻塞等待。

实际上我们可以让父进程不要一直等待子进程退出,而是当子进程未退出时父进程可以做一些自己的事情,当子进程退出时再读取子进程的退出信息,即非阻塞等待。

我们将options参数设置为WNOHANG:指定非阻塞模式。如果没有子进程退出,立即返回,不挂起父进程。并且使用循环进行轮番检测,如果等待不成功,那父进程就去做别的事情。过段时间再去调用waitpid函数,等待子进程成功后,父进程才会退出。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{pid_t id = fork();if (id == 0) {//childint count = 3;while (count--) {printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());sleep(3);}exit(0);}//fatherwhile (1) {int status = 0;pid_t ret = waitpid(id, &status, WNOHANG);if (ret > 0) //父进程等待成功{printf("wait child success...\n");printf("exit code:%d\n", WEXITSTATUS(status));break;}else if (ret == 0) //子进程仍在运行{printf("father do other things...\n");//可以穿插其他函数让父进程去做其他事情sleep(1);}else //waitpid返回-1,等待失败,终止等待{printf("waitpid error...\n");break;}}return 0;
}

 四、进程替换

进程替换原理:

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

当一个进程执行了进程替换(如exec()系列函数)后,它会被替换为另一个程序。

以下函数统称exec函数: 

  1. execl():

    • 函数接口:int execl(const char *path, const char *arg0, ... /* (char *) NULL */);
    • 参数解释:
      • path:要执行的程序的路径。
      • arg0:程序的名称,一般来说是 path 的基本文件名。
      • ...:传递给新程序的参数列表,以 NULL 结尾。
  2. execle():

    • 函数接口:int execle(const char *path, const char *arg0, ..., /* (char *) NULL, char *const envp[] */);
    • 参数解释:
      • 除了 execl() 的参数外,还接受一个额外的参数 envp,用于传递环境变量数组。
  3. execlp():

    • 函数接口:int execlp(const char *file, const char *arg0, ... /* (char *) NULL */);
    • 参数解释:
      • file:要执行的程序的文件名,会在环境变量 PATH 指定的路径中搜索。
      • 其他参数与 execl() 类似。
  4. execv():

    • 函数接口:int execv(const char *path, char *const argv[]);
    • 参数解释:
      • path:要执行的程序的路径。
      • argv[]:传递给新程序的参数数组,以 NULL 结尾。
  5. execvp():

    • 函数接口:int execvp(const char *file, char *const argv[]);
    • 参数解释:
      • file:要执行的程序的文件名,会在环境变量 PATH 指定的路径中搜索。
      • argv[]:传递给新程序的参数数组,以 NULL 结尾。
  6. execve():

    • 函数接口:int execve(const char *filename, char *const argv[], char *const envp[]);
    • 参数解释:
      • filename:要执行的程序的路径。
      • argv[]:传递给新程序的参数数组,以 NULL 结尾。
      • envp[]:环境变量数组。
  7. execvpe():

    • 函数接口:int execvpe(const char *file, char *const argv[], char *const envp[]);
    • 参数解释:
      • file:要执行的程序的文件名,会在环境变量 PATH 指定的路径中搜索。
      • argv[]:传递给新程序的参数数组,以 NULL 结尾。
      • envp[]:环境变量数组。

这些函数的参数解释中,path代表要执行的程序的路径, file 代表要执行的可执行文件的文件名,argv[] 是传递给新程序的参数数组,而 envp[] 是环境变量数组。函数的返回值为 -1 表示执行失败,具体的错误信息可以通过查看 errno 来获取。进程替换如果调用成功则加载新的程序从启动代码开始执行,不再返回到原来的程序中。 

命名理解

  • 这些函数用于执行其他程序。
  • 以 “l(list)” 结尾的函数(如 execl、execle)采用参数列表的方式传递参数,参数个数在函数调用时需要提前确定。
  • 以 “v(vector)” 结尾的函数(如 execv、execvp)采用指针数组的方式传递参数,参数个数在数组的结束标志(NULL)前确定。
  • 以 “p(path)” 结尾的函数(如 execlp、execvp、execvpe)可以根据环境变量 PATH 来搜索可执行文件。
  • 以 “e(env)” 结尾的函数(如 execle、execve、execvpe)允许设置环境变量。
  • execvpe() 在搜索可执行文件时除了搜索 PATH 环境变量外,还可以通过传递一个环境变量数组来搜索。

应用举例: 

#include <unistd.h>
int main()
{char* const argv[] = { "ps", "-ef", NULL };char* const envp[] = { "PATH=/bin:/usr/bin", "TERM=console", NULL };execl("/bin/ps", "ps", "-ef", NULL);// 带p的,可以使用环境变量PATH,无需写全路径execlp("ps", "ps", "-ef", NULL);// 带e的,需要自己组装环境变量execle("ps", "ps", "-ef", NULL, envp);execv("/bin/ps", argv);// 带p的,可以使用环境变量PATH,无需写全路径execvp("ps", argv);// 带e的,需要自己组装环境变量execve("/bin/ps", argv, envp);exit(0);
}

 事实上,只有execve才是真正的系统调用,其它五个函数最终都是调用的execve,所以execve在man手册的第2节,而其它五个函数在man手册的第3节,也就是说其他五个函数实际上是对系统调用execve进行了封装,以满足不同用户的不同调用场景的。

相关文章:

【Linux】进程管理(2):进程控制

一、进程创建&#xff1a;fork函数 我们在命令行中输入man fork 即可得到fork函数的函数接口的函数的使用方法。 我们可以看到&#xff0c;fork函数位于man手册的第2部分&#xff0c;由于第2部分通常是用于描述系统调用和库函数&#xff0c;所以我们可以了解到fork函数实际是一…...

组合数(费马小定理, 快速幂)

给定 n 组询问&#xff0c;每组询问给定两个整数 a&#xff0c;b&#xff0c;请你输出 Cbamod(1097)的值。 输入格式 第一行包含整数 n。 接下来 n 行&#xff0c;每行包含一组 a 和 b。 输出格式 共 n 行&#xff0c;每行输出一个询问的解。 数据范围 1≤n≤10000, 1≤…...

VMware Esxi安装群辉系统

群晖的网络存储产品具有强大的操作系统&#xff0c;提供了各种应用程序和服务&#xff0c;包括文件共享、数据备份、多媒体管理、远程访问等。用户可以通过简单直观的界面来管理他们的存储设备&#xff0c;并且可以根据自己的需求扩展设备的功能。总的来说&#xff0c;群晖的产…...

arm交叉编译器工具

下载地址&#xff1a; Builds & Downloads | Linaro 进入首页后&#xff0c;点击" GNU Toolchain Integration Builds" 有以下版本&#xff1a; 根据自己的选择下载对应的版本&#xff0c;本例选择14.0-2023.06-1 根据板端对应的版本选择相应的下载 比如下载3…...

Dajngo -- 表单

表单 form 此后的 app 名以 polls 为例 <h1>{{ question.question_text }}</h1>{% if error_message %} <p> <strong>{{ error_message }}</strong> </p> {% endif %}<form action"{% url polls:vote question.id %}" method…...

NIO基础知识

在学习Netty之前先要学习一下NIO相关的知识&#xff0c;因为Netty是基于NIO搭建的一套网络编程框架。 一. NIO 基础 non-blocking io 非阻塞 IO 1. 三大组件 1.1 Channel & Buffer channel 有一点类似于 stream&#xff0c;它就是读写数据的双向通道&#xff0c;可以从…...

C语言正则表达式 regnext regreplace regreplaceAll

由于C语言的正则表达式API相对比较简易.默认API只有regcomp/regerror/regexec/regfree这些函数.相对于其他的高级语言中正则表达式所所能实现的功能(如:查找/替换)有所欠缺.所以想着自己写下一些需要的函数以备后续需要使用. #ifndef _E_REGEX_INCLUDE #define _E_REGEX_INCLU…...

使用aspose相关包将excel转成pdf 并导出

SpringBoot 项目 基于aspose相关jar包 将excel 转换成pdf 导出 1、依赖的jar包 &#xff0c; jar获取链接 aspose相关三方jar &#xff0c;下载解压后,在项目路径下建一个libs包&#xff0c;然后将下图两个jar 拷贝至刚新建的libs目录中 2、pom.xml中加入maven引入 <depend…...

按关键字搜索商品API接口搜索关键字,显示商品总数,标题,图片,优惠价参数等

按关键字搜索商品API接口通常用于根据关键字搜索商品&#xff0c;并返回商品的相关信息。以下是一个示例&#xff0c;说明如何使用Python调用按关键字搜索商品API接口。 item_search-按关键字搜索淘宝商品 公共参数 API接口请求地址:调用key 名称类型必须描述keyString是调用…...

网络基础知识入门

目录 一、局域网与广域网 1、局域网 2、广域网 二、协议 1、概念 2、协议的理解 3、协议的分层 1、分层 2、OSI七层模型 三、网络传输基本流程 1、报头 2、局域网通信原理 3、跨网络传输流程 四、IP地址和MAC地址 1、IP地址 2、MAC地址 3、两者的区别 一、局域…...

D435i发布的话题学习

参考自https://blog.csdn.net/sinat_16643223/article/details/136406602?spm1001.2014.3001.5502 这里整理一下D435i庞大的topic话题数据。 根据算法来说的话&#xff0c;vins-fusion需要双目灰度图像的话题&#xff1a; camera/infra1/image_rect_raw 和camera/infra2/image…...

Springboot启动过程

加载配置&#xff1a;Spring Boot会加载应用程序的配置文件&#xff0c;包括application.properties或application.yml等。这些配置文件中包含了应用程序的各种配置信息&#xff0c;如数据库连接、端口号等。 创建Spring容器&#xff1a;Spring Boot会创建一个Spring容器&…...

网络安全之命令注入

漏洞原理&#xff1a; 应用系统设计需要给用户提供指定的远程命令操作的接口&#xff0c;比如&#xff1a;路由器&#xff0c;防火墙&#xff0c;入侵检测等设备的web管理界面。一般会给用户提供一个ping操作的web界面 用户从web界面输入目标IP&#xff0c;提交后台会对改IP地…...

使用GDAL进行简单的坐标系转换

使用GDAL进行简单的坐标系转换 使用python GDAL进行简单的坐标系转换&#xff0c;暂时不考虑不同基准坐标系转换的精度问题。 安装环境 使用UbuntuAnaconda python 环境 conda install gdal 定义坐标系 from osgeo import gdal from osgeo import osrsrs_wgs84 osr.Spati…...

【AIGC调研系列】AI大模型结合迁移学习进行微调的应用

AI大模型结合迁移学习进行微调的应用主要体现在通过预训练模型快速适应新任务&#xff0c;提高模型性能和准确性。迁移学习允许我们利用在其他任务上学到的知识来加速新任务的学习过程&#xff0c;从而减少对大量标注数据的依赖&#xff0c;提高训练效率[1][2][3]。在AI领域&am…...

低代码革新:软件开发的未来潜力与创新路径探索

过去的一年&#xff0c;挑战与机遇并存。人们一边忧虑市场经济下行所带来的新的增长难题、裁员危机&#xff0c;一边惊叹于AIGC、量子技术等领域不断涌现新的创新成果。 时代发生了改变&#xff0c;传统“互联网”的模式已走入尾声&#xff0c;新一轮的科技革命与产业变革正在到…...

AI智能校色解决方案,专业级画质提升

由于拍摄环境、设备性能以及编辑经验等多种因素的影响&#xff0c;视频画质往往难以达到理想状态。这时&#xff0c;一款高效、智能的校色解决方案就显得尤为重要。美摄科技凭借深厚的图像处理技术和AI算法研发实力&#xff0c;推出了全新的AI智能校色解决方案&#xff0c;助力…...

面试算法-148-轮转数组

题目 给定一个整数数组 nums&#xff0c;将数组中的元素向右轮转 k 个位置&#xff0c;其中 k 是非负数。 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3 输出: [5,6,7,1,2,3,4] 解释: 向右轮转 1 步: [7,1,2,3,4,5,6] 向右轮转 2 步: [6,7,1,2,3,4,5] 向右轮转 3 步: [5,6,7,…...

Linux——静态库 共享库

1.库文件 1).库文件 库是一组预先编译好的方法的集合; Linux系统存储库的位置一般在/lib 和 /usr/lib (64位系统/usr/lib64) 库的头文件放在/usr/include 2).库的分类 静态库:libxxx.a(命名规则) 共享库:libxxx.so(命名规则) 3).准备文件: //add.c int add(int x,int y) { re…...

基于Spring Boot+Vue的在线拍卖系统

随着社会的发展&#xff0c;社会的各行各业都在利用信息化时代的优势。计算机的优势和普及使得各种信息系统的开发成为必需。 在线拍卖系统&#xff0c;主要的模块包括管理员&#xff1b;首页、个人中心、用户管理、商品类型管理、拍卖商品管理、历史竞拍管理、竞拍订单管理、…...

Unity构建详解(6)——SBP的Bundle写操作生成

以下三个操作实际上是为了得到构建Bundle需要的其他参数&#xff0c;最关键的Bundle组装参数在上文已经说过了&#xff0c;至于这三个操作的具体细节不用过于追究&#xff0c;一般不怎么会取修改。 这些参数采用命令模式被封装起来&#xff0c;这是常见的参数非常多的时候的处…...

2024新版PHP在线客服系统多商户AI智能在线客服系统源码机器人自动回复即时通讯聊天系统源码PC+H5

搭建环境&#xff1a; 服务器 CPU 2核心 ↑ 运存 2G ↑ 宽带 5M ↑ 服务器操作系统 Linux Centos7.6-7.9 ↑ 运行环境&#xff1a; 宝塔面板 Nginx1.18- 1.22 PHP 7.1-7.3 MYSQL 5.6 -5.7 朵米客服系统是一款全功能的客户服务解决方案&#xff0c;提供多渠道支持…...

使用GPT需要注意的事项

GPT出来之后&#xff0c;基本就告别浏览器搜索问题答案了。将问题原封不动的copy给GPT基本可以得到解答。 但是这个也有弊端&#xff0c;那就是太依赖GPT了。 1&#xff0c;使用GPT需要更强的专业知识&#xff1a;除了能问对问题&#xff0c;还要具备识别GPT&q…...

学习人工智能:为何PyTorch深度学习框架不可或缺

在人工智能&#xff08;AI&#xff09;的浩瀚领域中&#xff0c;深度学习作为其核心分支&#xff0c;正以其强大的数据处理能力、模式识别能力和预测能力引领着科技的飞速发展。而在深度学习的众多工具与框架中&#xff0c;PyTorch无疑是一颗璀璨的明星。本文将从PyTorch的特点…...

Python基于Tkinter的加法游戏

定制魏:QTWZPW,获取更多源码等 目录 题目概述 详细设计 函数设计 总体设计 重要代码...

基于Springboot4S店车辆管理系统

采用技术 基于Springboot4S店车辆管理系统的设计与实现~ 开发语言&#xff1a;Java 数据库&#xff1a;MySQL 技术&#xff1a;SpringBootMyBatis 工具&#xff1a;IDEA/Ecilpse、Navicat、Maven 页面展示效果 管理员功能 首页 销售员管理 维修员管理 客户管理 供应…...

深入浅出 -- 系统架构之分布式架构

​​​​​​分布式架构&#xff1a; 根据业务功能对系统做拆分&#xff0c;每个业务功能模块作为独立项目开发&#xff0c;称为一个服务。 当垂直应用越来越多时&#xff0c;应用之间的交互不可避免&#xff0c;可将共用的基础服务或核心模块抽取出来作为独立服务&#xff0c…...

6. Z 字形变换(Java)

目录 题目描述&#xff1a;输入&#xff1a;输出&#xff1a;代码实现&#xff1a; 题目描述&#xff1a; 将一个给定字符串 s 根据给定的行数 numRows &#xff0c;以从上往下、从左到右进行 Z 字形排列。 比如输入字符串为 “PAYPALISHIRING” 行数为 3 时&#xff0c;排列如…...

【Linux入门】用户的基本指令

Linux操作系统又被称之为“核心&#xff08;kernel&#xff09;” 。但一般的用户&#xff0c;不能直接使用kernel&#xff0c;而要通过kernel的“外壳”程序——命令行解释器shell&#xff0c;来与 kernel 沟通。这是因为 kernel 对于一般的用户来说太过复杂&#xff0c;且不让…...

3.9 Python格式化字符串

Python格式化字符串&#xff08;格式化输出&#xff09; 我们之前讲到过 print() 函数的用法&#xff0c;这只是最简单最初级的形式&#xff0c;print() 还有很多高级的玩法&#xff0c;比如格式化输出&#xff0c;这就是本节要讲解的内容。 熟悉C语言 printf() 函数的读者能够…...

自己搭建聊天软件/浙江seo外包

UE4视频教程进行到了mysql(1.1)&#xff0c;tf1(1.1),oss(9.4),蓝图反射(1.3)&#xff0c; 终于稳下心来了&#xff0c; 5月29日&#xff0c;终于稳下来了&#xff0c;哈哈。全员降薪真是挫伤积极性的大杀器。 8&#xff1a;27–8&#xff1a;36&#xff0c;8&#xff1a;45-9…...

石家庄西晨网站开发/seo怎么做新手入门

一、命令格式1.命令 选项 对象2.选项 短格式: -h&#xff0c;可操作性强 长格式: --help&#xff0c;可读性强3.命令提示符 $ 普通用户 # 超级用户二、获得命令帮助1.help 命令&#xff0c;查看内部命令的帮助2.--help选项&#xff0c;如ls --help 外部命令3.man命令&…...

免费网站模板网/企业文化是什么

本来是想纯写技术文章的&#xff0c;算了&#xff0c;还是搞学习记录就行&#xff0c;太懒了这个是js获取经纬度方法function getMyLocationAndSign() { var geolocation new BMap.Geolocation();// 定位当前位置 geolocation.getCurrentPosition(function(r) { if (this.getS…...

如何做网站的源码/谷歌搜索引擎怎么才能用

闭包 闭包定义&#xff1a;指拥有多个变量和绑定了这些变量的环境的表达式&#xff08;通常是一个函数&#xff09;&#xff0c;因而这些变量也是该表达式的一部分。函数内部可以直接读取全局变量。函数内部变量无法在函数外部访问。函数内部声明要用var或者let声明&#xff0c…...

菏泽做网站的/seo短视频

在《Code Complete》这本书中&#xff0c;作者记录了这样一个故事&#xff1a; 有位负责维护的程序员半夜被叫起来&#xff0c;去修复一个出了问题的程序。但是程序的原作者已经离职&#xff0c;没有办法联系上他。这个程序员从未接触过这个程序。在仔细检查所有的说明后&#…...

龙岗网站建设 公司推广/微信公众号seo

KVC之-setValue:forKey:方法实现原理与验证 - (void)setValue:(id)value forKey:(NSString *)key方法,实现原理与验证 功能:使用一个字符串标示符给一个对象的属性赋值.它支持普通对象和集合对象 这个方法的默认实现如下: (1).首先去接收者(调用方法的那个对象)的类中查找与key…...