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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/510934.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【总结】在嵌入式设备上可以离线运行的LLM--Llama

文章目录 Llama 简介运用另一种&#xff1a;MLC-LLM 一个令人沮丧的结论在资源受限的嵌入式设备上无法运行LLM&#xff08;大语言模型&#xff09;。 一丝曙光&#xff1a;tinyLlama-1.1b&#xff08;10亿参数&#xff0c;需要至少2.98GB的RAM&#xff09; Llama 简介 LLaMA…

自动驾驶的世界模型:综述

自动驾驶的世界模型&#xff1a;综述 附赠自动驾驶学习资料和量产经验&#xff1a;链接 24年3月澳门大学和夏威夷大学的论文“World Models for Autonomous Driving: An Initial Survey”。 在快速发展的自动驾驶领域&#xff0c;准确预测未来事件并评估其影响的能力对安全性…

ssm017网上花店设计+vue

网上花店的设计与实现 摘 要 网络技术和计算机技术发展至今&#xff0c;已经拥有了深厚的理论基础&#xff0c;并在现实中进行了充分运用&#xff0c;尤其是基于计算机运行的软件更是受到各界的关注。加上现在人们已经步入信息时代&#xff0c;所以对于信息的宣传和管理就很关…

[云呐]固定资产盘点报告哪个部门写

固定资产盘点报告通常由哪个部门来完成和签发呢?总体来说,固定资产盘点报告主要由资产管理部门或核算部门具体组织拟定并与财务部门共同签发。个别重大报告还需要上级领导或委员会研讨通过。  资产管理部门:  资产管理部门是直接负责公司固定资产管理工作的核心部门,它主导…

超市销售数据-python数据分析项目

Python数据分析项目-基于Python的销售数据分析项目 文章目录 Python数据分析项目-基于Python的销售数据分析项目项目介绍数据分析结果导出数据查阅 数据分析内容哪些类别比较畅销?哪些商品比较畅销?不同门店的销售额占比哪个时间段是超市的客流高封期?查看源数据类型计算本月…

浅谈iOS开发中的自动引用计数ARC

1.ARC是什么 我们知道&#xff0c;在C语言中&#xff0c;创建对象时必须手动分配和释放适量的内存。然而&#xff0c;在 Swift 中&#xff0c;当不再需要类实例时&#xff0c;ARC 会自动释放这些实例的内存。 Swift 使用 ARC 来跟踪和管理应用程序的内存&#xff0c;其主要是由…

EFPN代码解读

论文 Extended Feature Pyramid Network for Small Object Detection python3 D:/Project/EFPN-detectron2-master/tools/train_net.py --config-file configs/InstanceSegmentation/pointrend_rcnn_R_50_FPN_1x_coco.yaml --num-gpus 1 训练脚本 cfg 中的配置 先获取配置…

JavaWeb 项目运行配置

JavaWeb 项目运行配置

保持ssh断开后,程序不会停止执行

保持ssh断开后&#xff0c;程序不会停止执行 一、前言 笔者做远程部署搞了一阵子&#xff0c;快结项时发现一旦我关闭了ssh连接窗口&#xff0c;远程服务器会自动杀掉我在ssh连接状态下运行的程序。 这怎么行&#xff0c;岂不是想要它一直运行还得要一台电脑一直打开ssh连接咯…

【优选算法专栏】专题十六:BFS解决最短路问题---前言

本专栏内容为&#xff1a;算法学习专栏&#xff0c;分为优选算法专栏&#xff0c;贪心算法专栏&#xff0c;动态规划专栏以及递归&#xff0c;搜索与回溯算法专栏四部分。 通过本专栏的深入学习&#xff0c;你可以了解并掌握算法。 &#x1f493;博主csdn个人主页&#xff1a;小…

【QingHub】企业级应用开发管理

QingHub 企业级应用开发设计器是QingHub Studio的一个核心模块&#xff0c;它可以实现应用搭建、团队管理&#xff0c;共享开发&#xff0c;可以快速接入API接口&#xff0c;复杂功能可以通过自定义脚本快速实现业务逻辑。打通前端开发与后台业务逻辑一体化。通过可视化的方式&…

使用 PDManer 对数据库表建模(建表语句生成,代码生成)

目录 前言 基本使用教程 新建项目 创建表 关系图 建表语句 生成代码 导入 前言 在软件开发中过程中&#xff0c;一般分为几个过程&#xff1a;需求分析、概要设计、详细设计、编码实现、软件测试和软件交付。 在概要设计和详细设计过程中&#xff0c;则需要对业务进…

苍穹外卖06(HttpClient,微信小程序开发,微信登录流程,获取授权码从微信平台获取用户信息)

目录 一、HttpClient 1. 介绍 2. 入门案例 1 导入依赖(已有) 2 GET方式请求 2 POST方式请求 二、微信小程序开发 1. 介绍 2. 准备工作 1 注册小程序获取AppID 注册小程序 完善小程序信息 2 下载并安装开发者工具 3 设置小程序开发者工具(必做) 3. 入门案例 1 小…

CentOS 7 下离线安装RabbitMQ教程

CentOS 7 下安装RabbitMQ教程一、做准备&#xff08;VMWare 虚拟机上的 CentOS 7 镜像 上安装的&#xff09; &#xff08;1&#xff09;准备RabbitMQ的安装包&#xff08;rabbitmq-server-3.8.5-1.el7.noarch&#xff09;下载地址mq https://github.com/rabbitmq/rabbitmq-se…

基于51单片机的简易计算器设计

1、任务 本课题模拟计算器设计硬件电路采用三部分电路模块构成&#xff0c; 第一部分是键盘模块电路&#xff0c;采用4*4矩阵式键盘作为输入电路&#xff1b; 第二部分是LCD1602液晶显示模块&#xff1b; 第三部分是以51单片机作为控制核心。 软件程序主要由三部分组成&am…

AWS-EKS 给其他IAM赋予集群管理权限

AWS EKS 设计了权限管理系统&#xff0c;A用户创建的集群 B用户是看不到并且不能管理和使用kubectl的&#xff0c;所以我们需要共同管理集群时就需要操场共享集群访问给其他IAM用户。 两种方式添加集群控制权限&#xff08;前提&#xff1a;使用有管理权限的用户操作&#xff…

子集与全排列问题(力扣78,90,46,47)

系列文章目录 子集和全排列问题与下面的组合都是属于回溯方法里的&#xff0c;相信结合前两期&#xff0c;再看这篇笔记&#xff0c;更有助于大家对本系列的理解 一、组合回溯问题 二、组合总和问题 文章目录 系列文章目录题目子集一、思路二、解题方法三、Code 子集II一、思…

基于SSM的网上打印管理

摘要 进入二十一世纪以来&#xff0c;计算机技术蓬勃发展&#xff0c;人们的生活发生了许多变化。很多时候人们不需要亲力亲为的做一些事情&#xff0c;通过网络即可完成以往需要花费很多时间的操作&#xff0c;这可以提升人们的生活质量。计算机技术对人们生活的改变不仅仅包…

不会还有程序员不知道这几个宝藏学习平台吧?还不来码住!

咱作为程序员可谓是赶上了发展的时代啊&#xff01;前有ChatGPT&#xff0c;后有5G、物联网等等层出不穷。这正是咱大展身手、大赚一笔的好时候啊&#xff01;有多少人趁着风口期大干一笔&#xff0c;狠狠投入&#xff0c;最终不说是top级别&#xff0c;也至少是羡煞众人啊&…

最新AI智能系统ChatGPT网站源码V6.3版本,GPTs、AI绘画、AI换脸、垫图混图+(SparkAi系统搭建部署教程文档)

一、前言 SparkAi创作系统是基于ChatGPT进行开发的Ai智能问答系统和Midjourney绘画系统&#xff0c;支持OpenAI-GPT全模型国内AI全模型。本期针对源码系统整体测试下来非常完美&#xff0c;那么如何搭建部署AI创作ChatGPT&#xff1f;小编这里写一个详细图文教程吧。已支持GPT…