一、进程创建: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()
函数主要完成以下操作:
创建新的进程控制块(Process Control Block,PCB):内核会为新的子进程分配一个唯一的进程标识符(PID),并在内存中为其创建一个新的进程控制块(PCB)。这个 PCB 将包含子进程的运行状态、程序计数器、堆栈指针、文件描述符等信息。
复制父进程的地址空间以创建自己的地址空间:在大多数情况下,
fork()
函数会创建子进程的完整副本,包括代码段、数据段、堆栈等。这意味着子进程将会获得与父进程几乎完全相同的内存映像。这一步通常通过 Copy-On-Write(写时复制)技术来实现,即在子进程需要修改内存时才会进行实际的复制操作。将子进程的状态设置为就绪:一旦子进程的地址空间准备好,内核将其状态设置为就绪态,以便在合适的时机可以被调度执行。
返回不同的值:在内核中,
fork()
函数会返回两次,一次是在父进程的上下文中返回子进程的 PID,另一次是在子进程的上下文中返回 0。这样,父进程和子进程可以根据返回值来执行不同的代码路径。
在fork函数内部,在执行 return pid 之前,子进程就已经创建完成,所以 return pid 实际也是父子进程的共享代码部分,所以父进程会执行一次,返回子进程的pid;而子进程也会执行一次 return pid 返回进程是否创建完成的信息。
2、为什么父进程接收子进程的PID,而子进程返回0或-1?
父进程接收子进程的PID:父进程在调用
fork()
函数后,会得到子进程的PID作为返回值。通过这个PID,父进程可以对子进程进行跟踪、管理和通信。例如,父进程可能会使用子进程的PID来等待子进程的结束状态(通过waitpid()
函数),或者向子进程发送信号(通过kill()
函数)等。子进程返回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 进程接管孤儿进程的管理。孤儿进程的退出方式与其他进程相同。系统关闭:当系统关闭或重启时,所有正在运行的进程都会被终止。操作系统通常会向所有进程发送信号,以便它们有机会在关闭之前执行清理操作。
其他。。。
进程退出方法:
调用 exit()、_exit() 函数
在main()函数中使用return语句返回
Ctrl + c 组合键 或者 使用 kill 命令搭配终止信号
exit()函数、_exit()函数详解
exit()
函数和_exit()
函数都是用于终止程序的执行
exit()
函数:
exit()
函数是标准 C 库中的函数,用于正常终止程序的执行,并返回退出状态给操作系统。- 调用
exit()
函数会执行以下步骤:
- 执行所有注册的退出处理程序(使用
atexit()
注册的函数)。- 关闭所有打开的流(文件)。
- 刷新所有的缓冲区。
- 返回退出状态给操作系统。
exit()
函数的原型在<stdlib.h>
头文件中声明,其函数原型如下:#include <stdlib.h> void exit(int status);
status
参数是传递给操作系统的退出状态,通常用来指示程序的结束状态,一般约定 0 表示成功,非零值表示失败或其他特定状态。exit()
函数在正常终止程序时应该被调用,它会执行标准的程序清理操作,并返回状态给操作系统。
_exit()
函数:
_exit()
函数是系统调用,用于立即终止程序的执行,不执行标准的程序清理操作。- 调用
_exit()
函数会立即终止程序的执行,并且不会执行以下操作:
- 不执行注册的退出处理程序。
- 不关闭打开的流(文件)。
- 不刷新缓冲区。
_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){
//child
int count = 10;
while(count--){
printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
//father
int status = 0;
pid_t ret = wait(&status);
//pid_t ret = wait(-1, &status, 0); 此方法同上
if(ret > 0){
//wait success
printf("wait child success...\n");
if(WIFEXITED(status)){
//exit normal
printf("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){
//child
int count = 20;
while(count--){
printf("I am child...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
//father
int status = 0;
pid_t ret = waitpid(-1, &status, 0);
if(ret > 0){
//wait success
printf("wait child success...\n");
//判断是否正常退出
if(WIFEXITED(status)){
//exit normal
printf("exit code:%d\n", WEXITSTATUS(status));//正常退出则打印退出码
}
else {
//exit abnormal
printf("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)
{ //child
printf("child is run, pid is : %d\n", getpid());
sleep(5);
exit(257);
}
else
{
int status = 0;
pid_t ret = waitpid(-1, &status, 0);//阻塞式等待,等待5S
printf("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)
{
//child
int count = 3;
while (count--)
{
printf("child do something...PID:%d, PPID:%d\n", getpid(), getppid());
sleep(3);
}
exit(0);
}
//father
while (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函数:
execl():
- 函数接口:
int execl(const char *path, const char *arg0, ... /* (char *) NULL */);
- 参数解释:
path
:要执行的程序的路径。arg0
:程序的名称,一般来说是path
的基本文件名。...
:传递给新程序的参数列表,以 NULL 结尾。execle():
- 函数接口:
int execle(const char *path, const char *arg0, ..., /* (char *) NULL, char *const envp[] */);
- 参数解释:
- 除了 execl() 的参数外,还接受一个额外的参数
envp
,用于传递环境变量数组。execlp():
- 函数接口:
int execlp(const char *file, const char *arg0, ... /* (char *) NULL */);
- 参数解释:
file
:要执行的程序的文件名,会在环境变量 PATH 指定的路径中搜索。- 其他参数与 execl() 类似。
execv():
- 函数接口:
int execv(const char *path, char *const argv[]);
- 参数解释:
path
:要执行的程序的路径。argv[]
:传递给新程序的参数数组,以 NULL 结尾。execvp():
- 函数接口:
int execvp(const char *file, char *const argv[]);
- 参数解释:
file
:要执行的程序的文件名,会在环境变量 PATH 指定的路径中搜索。argv[]
:传递给新程序的参数数组,以 NULL 结尾。execve():
- 函数接口:
int execve(const char *filename, char *const argv[], char *const envp[]);
- 参数解释:
filename
:要执行的程序的路径。argv[]
:传递给新程序的参数数组,以 NULL 结尾。envp[]
:环境变量数组。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进行了封装,以满足不同用户的不同调用场景的。