【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){
		//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函数: 

  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进行了封装,以满足不同用户的不同调用场景的。

  • 28
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小白也有开发梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值