【Linux系统】进程替换

文章目录

  • 前言
  • [1. 接口介绍](#1. 接口介绍)
  • [2. 案例分析](#2. 案例分析)
  • [3. 原理分析](#3. 原理分析)

前言

本文章,小编会和大家一起探讨关于进程替换的接口和细节。通过学习了解进程替换,我们就能够明白:命令行参数、环境变量表的传递,和bash命令行上执行命令。

1. 接口介绍

在正式进入介绍接口之前,我们先来看看,一个进程替换的现象:

c 复制代码
int main()
{
	printf("I am a process pid:%d\n", getpid());

	execl("/usr/bin/ls", "ls", "-a", "-l", NULL); // 必须以NULL结尾

	printf("我已经执行到这里了\n");

	return 0;
}
  • 来看下面的运行结果:

    现象就是:调用了execl函数之后的代码并没有执行。

下面我们正式来认识进程替换的接口:

这些接口exec*系列的库函数

上面一共有5个接口(execvpe未被声明),记忆比较麻烦。这里给出一点提示:

  1. l】:可以表示成为list,意思就是传参的时候需要像命令行参数一样传入。
  2. p】:可以表示执行的文件可以 不用带路径 ,进行程序替换的时候会从环境变量PATH中找该文件。
  3. e】:表示成为environment ,表示在传参的时候传入一个环境变量表
  4. v】:表示成为vector,表示指令和选项以指针数组的形式传入。。

注意:不管是l、v,最后必须以NULL结尾,这是规定。

2. 案例分析

接下来,我们通过几个案例来说明这几个接口的使用

  • 案例一:使用execl替换成为ls指令
c 复制代码
int main()
{
	// 1、"/usr/bin/ls":表示我要执行谁,我执行的文件在哪里?
	// 2、 "ls", "-a", "-l", "NULL":表示我要怎么执行。和命令行类似
	execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
	return 0;
}
  • 案例二:使用execlp替换成功ls指令
c 复制代码
int main()
{
	// 1、"ls":表示我要执行谁,因为我们带了【p】所以会在环境变量下找
	// 2、 "ls", "-a", "-l", "NULL":表示我要怎么执行。
	execlp("ls", "ls", "-a", "-l", NULL);
	return 0;
}
  • 案例三:创建子进程,子进程使用execvp替换成为ls指令。
c 复制代码
int main()
{
	char* const cmd[] = {
		"ls",
		"-a",
		"-l",
		NULL
	};

	pid_t pid = fork();
	if(pid == 0)
	{
		//子进程
		printf("I am a child pid:%d, ppid:%d\n", getpid(), getppid());
		sleep(1);
		//进行进程替换
		execvp("ls", cmd);

		printf("I am here\n");
		exit(10); //故意设置
	}
	//父进程
	int status = 0;
	pid_t ret = waitpid(pid, &status, 0);
	if(ret > 0)
	{
		printf("I am father, I wait child's pid:%d, ret:%d\n", pid, ret);
		if(WIFEXITED(status))
		{
			printf("child exit code: %d\n", WEXITSTATUS(status));
		}
	}

	return 0;
}
  • 现象

    1. 程序替换之后,后续的代码并没有执行。

    2. 程序替换之后,子进程的pid没有改变。

    3. 程序替换之后,父子进程的关系并没有改变。

  • 问题及结论

    1. 子进程被程序替换之后不会影响父进程吗?为什么?代码和数据是如何处理的?

      子进程程序替换之后不会影响父进程。因为进程具有独立性 。父子进程指向的同一份代码,会发生"写时拷贝"。对于进程替换来说,仅仅是改变进程的代码和数据,并且将PC指针指向自己的程序启动的起始地址。

    2. 子进程在程序替换之后的代码为什么没有执行?

      发生程序替换成功之后,子进程的数据和代码已经替换为需要执行的代码和数据了,原始想要执行的代码已经找不到了。如果程序替换失败,才会执行后续代码。

    3. CPU是如何得知,进程替换之后,新进程的代码入口地址呢?

      在Linux下,每一个可执行程序都会有严格的格式【ELF 】,其中表头就包含这一个程序的入口地址 。当发生进程替换之后,原来子进程的task_struct中的一些属性字段就会发生改变,例如页表指针、mm_struct指针、还有维护的一些关于CPU硬件资源的字段都会发生替换。这些属性的替换,都后面的CPU执行都有关系......

    4. 进程替换可以替换哪些可执行文件?

      我们不仅仅可以执行一些系统的指令,也可以执行我们自己所写的代码、脚本。这是具有跨语言性的,例如我们可以跑Python脚本,Shell的脚本。在Linux,程序本质都是进程,只要被执行起来了,Linux就为其分配PCB资源。

3. 原理分析

实际上,上面认识的接口都是对系统调用execve的封装

  • 系统调用和库函数关系图:

  • 下面我们正式来认识进程替换的原理:

进程替换的核心函数是execve()。它的作用不是创建一个新进程,而是将当前正在调用的进程(子进程)的代码和数据完全替换为一个新的程序

  1. 验证与准备

    当进程调用 execve() 时。内核首先检查该进程是否有权限执行这个操作。然后内核会找到磁盘上的目标ELF程序文件,并解析其格式,读取文件头信息(如代码段、数据段的大小、入口地址等)。

  2. 销毁旧的地址空间(除PCB外)

    这是"替换"的本质。内核会释放当前进程虚拟内存地址空间 中除了PCB(内核部分)之外的所有资源:释放代码段、数据段、堆、栈所占用的所有物理内存页和虚拟内存映射。(文件部分 :关闭所有通过fcntl标记为CLOEXEC(执行时关闭)的文件描述符。默认情况下,所有打开的文件描述符会被保留,这是实现输入输出重定向的基础。信号部分:对于通过signal设置的handel方法会清除,因为程序替换之后原来的地址已经失效了。同时如何设置为忽略会保存)

  3. 建立新的地址空间并加载新程序

    内核根据解析到的ELF文件信息,为新的程序重新设置虚拟内存布局 (进程地址空间),创建新的代码段、数据段、堆、栈的映射关系。代码段和数据段的映射非常巧妙:内核并不是立即将整个程序文件从磁盘读入物理内存惰性加载机制 )。为新的代码段和数据段建立内存映射,将它们映射到磁盘上的ELF文件本身。这意味着,在刚开始的时候,物理内存中可能只有新程序的一小部分(例如文件头)。后面程序运行触发缺页中断......

  4. 设置栈和堆

    栈: 内核会为新程序分配新的栈空间,并将命令行参数 (argv) 和环境变量 (envp) 压入新栈的顶部。(如果在系统调用之前,我们设置了新的环境变量,那么进程替换之后的环境变量表就是使用的是新的表)

    堆: 初始时堆的大小为零,随着新程序的执行(如调用 malloc),堆才会动态增长。

  5. 重置寄存器状态并开始执行

    内核将进程的程序计数器(PC/IP)等寄存器重置为新程序的入口点(_start,通常由C运行时库定义)。

    完成所有设置后,进程返回。此时,进程的上下文(代码、数据)已经完全变成了新程序的。CPU意识不到发生了替换,它只是从新的地址开始取指令执行。

完。希望这篇文章能够帮助你!

相关推荐
skyutuzz3 小时前
vim删除文本文件内容
linux·编辑器·vim
---学无止境---3 小时前
Linux信号处理的相关数据结构和操作函数
linux
ss2733 小时前
手写MyBatis第89弹:动态SQL解析与执行时机深度剖析
java·服务器·windows
霍格沃兹软件测试开发3 小时前
Playwright MCP浏览器自动化详解指南
运维·自动化
前行居士3 小时前
Sub-process /usr/bin/dpkg returned an error code (1)
linux·运维·windows
蒋星熠3 小时前
爬虫与自动化技术深度解析:从数据采集到智能运维的完整实战指南
运维·人工智能·爬虫·python·深度学习·机器学习·自动化
迎風吹頭髮4 小时前
UNIX下C语言编程与实践19-UNIX 三级索引结构:直接索引、一级/二级/三级间接索引的文件存储计算
运维·云计算·unix
数智顾问4 小时前
AI自动化测试:接口测试全流程自动化的实现方法——从需求到落地的全链路实践
运维·人工智能·自动化
tt5555555555555 小时前
Linux 驱动开发入门:LCD 驱动与内核机制详解
linux·运维·驱动开发