【Linux】进程控制(二) 深入理解进程程序替换与 exec 系列函数

【Linux】进程控制(二) 深入理解进程程序替换与 exec 系列函数

文章目录


一、进程程序替换

我们之前讲过fork () 之后,父子进程各自执行父进程代码的一部分,也就是代码共享,数据默认也“共享”,但是发生写入后就会以写时拷贝各自私有。那如果子进程想执行一个全新的程序成为一个真正独立的进程呢?这就需要通过进程的程序替换来完成这个功能!
程序替换是通过特定的系统调用接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中!

替换原理

进程替换原理很简单,就是进程调用某种系统调用,从磁盘中加载一份全新的代码和数据到该进程物理内存中,覆盖掉原进程在内存中的代码和数据。程序替换并没有创建新进程,只是改变了进程的物理内存。
在这里插入图片描述
我们空讲无用,小编打算先实际上手代码让大家见一见程序替换的效果,之后再回头结合原理讲解。
进程替换需要调用exec(注意不是excel表格)系列接口,一共有六个,还有一个接口我们后面补充:
在这里插入图片描述
我们先看最简单的excel:
intexecl(constchar* path,constchar* arg,...);
我们要执行一个程序首先要找到它,第一个参数就是用来帮助我们找到它,第二个参数是我们要执行程序的程序名,三个点表示可变参数,可填可不填,如果要填这部分参数指的是给程序传递的命令行选项,并且该部分参数传递完毕后必须以NULL结尾。
传递参数注意事项:除了path外,后面的参数你在命令行中怎么写,就在这里怎么传递。
下面直接上示例:
#include<stdio.h>#include<unistd.h>intmain(){printf("我是一个进程: %d\n",getpid());sleep(1);execl("/usr/bin/ls","ls","-a","-l","-n",NULL);printf("运行结束\n");return10;}
运行结果:
在这里插入图片描述
我们看到execl替换后的执行结果和ls命令一样,说明这样确实就可以让这个程序不执行自己的代码和数据,转而去执行ls的代码和数据。
但是这里还有个现象,替换后 printf(“运行结束\n”); 这条代码为什么没有运行了呢? 很容易理解,因为你的程序替换后开始执行另一个程序的代码了,你自己的代码已经被覆盖了。所以程序替换一但成功,后续代码不再执行,因为没有了!
那既然程序替换有成功,那也一定有失败,我们下面直接让程序替换失败来看现象:(执行一个不存在的指令就会失败)
#include<stdio.h>#include<unistd.h>intmain(){printf("我是一个进程: %d\n",getpid());sleep(1);int n =execl("/usr/bin/lllls","ls","-a","-l","-n",NULL);printf("运行结束, n = %d\n", n);return10;}
运行结果:
在这里插入图片描述
我们可以看到程序替换失败后后续代码还会正常执行,所以一但程序替换后续的代码被执行了,就表示程序替换失败。
我们还可以看到替换失败后execl返回-1,那如果替换成功还需要返回值吗?我们仔细想想,程序替换成功后execl的返回值就没有意义了,就算有后续代码也不会执行。所以程序替换如果成功,不需要、也不会有返回值!——>所以execl系列函数,一但返回,必然失败!
所以我们的代码应该这样写:
#include<stdio.h>#include<unistd.h>intmain(){printf("我是一个进程: %d\n",getpid());sleep(1);execl("/usr/bin/ls","ls","-a","-l","-n",NULL);printf("程序替换失败!\n");return10;}

子进程程序替换示例

有了上面的认识,我们再回过头看程序替换理论,前面都是替换当前进程的代码和数据,但我们一开始介绍程序替换概念的时候说程序替换是用来让子进程执行全新的代码的,所以接下来将介绍子进程是如何程序替换。
在开始编写代码之前,我们要先理解一些概念,子进程确实可以被替换,那么子进程替换后会影响父进程吗?我们知道进程之间具有独立性一定不会影响,但是先前不是讲的父子共用同一段代码吗?子进程替换数据我们知道会发生写时拷贝,其实子进程进行代码替换时操作系统也会进行类似写时拷贝的工作。
所以当子进程进行程序替换时,会把子进程的代码和数据加载进内存,而此时父子进程共享代码和数据,所以就会发生写时拷贝,系统会为子进程开辟新的物理内存,子进程的代码和数据就会加载进物理内存中,这样就保证了进程的独立性。
下面我们直接上代码,注意子进程替换后它本质还是那个子进程,当子进程执行完替换后的程序退出时也需要父进程来等待回收它。
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>intmain(){ pid_t id =fork();if(id ==0){//子进程sleep(2);printf("我是一个进程:%d\n",getpid());sleep(1);execl("/usr/bin/ls","ls","-a","-l",NULL);exit(1);}//父进程 pid_t rid =waitpid(id,NULL,0);if(rid >0){printf("wait: %d success\n", rid);}return0;}

加载器

小编这里再补充一些和历史知识的勾连,我们知道要运行一个二进制文件要先把它加载到内存,这是冯诺依玛体系结构规定的,因为CPU不能直接访问外设,只有加载进内存的代码和数据才能被CPU执行。加载是把数据从一个硬件加载到另一个硬件,所以一定需要操作系统来执行加载任务,只有操作系统有这个权力,所以加载底层一定会调用系统调用,那么对于linux而言,加载的本质其实就是调用程序替换的系统调用接口。
(很多人会疑惑:为什么 Linux 不能像 Windows 一样 “直接创建进程加载代码”?这是 Linux 的历史设计逻辑:fork()(创建子进程,复制父进程)和execve()(替换子进程)是分离的两个系统调用,这样可以在fork()和execve()之间插入额外逻辑(如修改环境变量、重定向输入输出),灵活性更高。而 Windows 的CreateProcess是 “一站式” 接口,将 “创建进程” 和 “加载程序” 合并,无需单独的程序替换步骤。)
加载器示例代码:
#include<stdio.h>#include<unistd.h>intmain(int argc,char* argv[]){printf("我是一个进程: %d\n",getpid());sleep(1);char** myargv =&argv[1];execv(myargv[0], myargv);printf("程序替换失败!\n");return10;}
运行结果:
在这里插入图片描述
示意图:
在这里插入图片描述
以上就是写的一个简易加载器程序,myexec就是加载器本体,可以通过它加载其他程序。

六个exec系列函数串讲

下面我们把exec系列系统调用串在一起讲,我们先梳理一下这批参数的共性,第一个参数是指你要执行谁,第二个以及后续参数是指你要如何执行,快速记忆就是在命令行上怎么写就在这里怎么填。
这些函数原型看起来很容易混,但只要掌握了规律就很好记:
在这里插入图片描述

execl:

intexecl(constchar*path,constchar*arg,...);
execl我们前面已经介绍过了,execl中的l表示list,因为传递参数是以一个一个单独的字符串传递的。第一个参数是要执行程序的路径,第二个参数我们以ls命令为例,可以写成 ls ,也可以写成 /usr/bin/ls ,后续可变参数是要ls命令的选项,最后一个参数以NULL结尾。
exec系列函数的所有代码小编都只贴上面子进程程序替换中的子进程内部逻辑,因为其他基本都一样。
if(id ==0){//子进程printf("我是一个进程: %d\n",getpid());sleep(1);execl("/usr/bin/ls","ls","-a","-l","-n",NULL);printf("程序替换失败!\n");exit(1);}

execv:

intexecv(constchar*path,char*const argv[]);
我们可以看到execv第二个参数是以字符串数组的方式传递的,所以execv中的v表示vector,用法和execl类似。数组的最后一个元素也要为NULL。
if(id ==0){//子进程printf("我是一个进程: %d\n",getpid());sleep(1);//"pwd"本质是const char*类型,// 需要把它强转为char*类型以匹配char* const类型的myargv数组char*const myargv[]={(char*)"pwd",NULL};execv("/usr/bin/pwd", myargv);printf("程序替换失败!\n");exit(1);}

execlp:

intexeclp(constchar*file,constchar*arg,...);
execlp除了第一个参数其他参数和execl一样。execl第一个参数传要执行程序的路径,而execlp第一个参数只用传要执行程序的程序名就行(比如 ls 和 ./mycmd ),代表的依旧是你要执行谁。原理就是使用execlp我们只用传要执行命令的名字,execlp自己会去环境变量path中寻找指定的程序并执行。
#include<stdio.h>#include<unistd.h>intmain(){printf("我是一个进程: %d\n",getpid());sleep(1);execlp("ls","ls","-a","-l","-n",NULL);printf("程序替换失败!\n");return10;}: 

execvp:

intexecvp(constchar* file,char*const argv[]);
有了前面的三个的介绍想必execvp各位都能理解了把,直接上代码。
if(id ==0){//子进程printf("我是一个进程: %d\n",getpid());sleep(1);execlp("ls","ls","-a","-l","-n",NULL);printf("程序替换失败!\n");exit(1);}

execvpe:

intexecvpe(constchar*file,char*const argv[],char*const envp[]);
带e的程序替换接口可以让程序员灵活地控制传递给替换后程序的环境变量,无论是自定义的环境变量还是系统的环境变量。
下面示例代码逻辑是myexec程序里创建一个子进程,然后子进程程序替换为mycmd程序,mycmd程序会打印它的命令行参数和环境变量。当我们直接运行mycmd时它会打印从父进程bash拿到的命令行参数和环境变量:
//mycmd.c#include<stdio.h>intmain(int argc,char* argv[],char* env[]){for(int i =0; argv[i]; i++){printf("argv[%d], %s\n", i, argv[i]);}for(int i =0; env[i]; i++){printf("env[%d], %s\n", i, env[i]);}return0;}
直接编译mycmd.c并运行:
在这里插入图片描述
当我们把自定义的命令行参数和环境变量通过execvpe传递给程序替换后的mycmd程序后mycmd就会打印出它拿到的我们自定义的命令行参数和环境变量:
//myexec.c#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>intmain(){ pid_t id =fork();if(id ==0){//子进程char*const myargv[]={"wusaqi","cmd",NULL};char*const myenv[]={"strggle=888","luck=666",NULL};execvpe("./mycmd", myargv, myenv);exit(0);}//父进程 pid_t rid =waitpid(id,NULL,0);if(rid >0){printf("wait: %d success\n", rid);}return0;}
myexec.c编译后的运行结果:
在这里插入图片描述
如果我们想把系统的环境变量传给替换后的程序,execcpe第三个参数就可以传environ。所以通过程序替换接口传递环境变量表默认意义是摒弃掉老的环境变量表,使用你自己设置的全新的环境变量表。如果程序替换不传环境变量表,替换后的新程序会默认使用调用 exec 函数的当前进程(即被替换的原进程)的环境变量表。
除了只传自定义的环境变量和传系统的环境变量,我们还可以既传系统环境变量,又传自定义的环境变量。这需要我们事先调用putenv传递自己的环境变量到系统的环境变量environ中,然后程序替换时传递environ。示例代码如下:
在这里插入图片描述
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>externchar** environ;char* my ="wusaqi=12345";intmain(){ pid_t id =fork();if(id ==0){//子进程char*const myargv[]={"wusaqi","cmd",NULL};putenv(my);execvpe("./mycmd", myargv, environ);exit(0);}//父进程 pid_t rid =waitpid(id,NULL,0);if(rid >0){printf("wait: %d success\n", rid);}return0;}

execle:

intexecle(constchar*path,constchar*arg,...,char*const envp[]);
这个接口小编就不细讲了。

第七个程序替换接口

我们之前介绍的六个exec系列函数都是man 3号手册的库函数,而接下来的这个execve是man 2号手册的系统调用,所有程序替换操作最后都会调用这个系统调用,把当前进程的命令行参数和环境变量传递给被替换的程序。也就是说上面介绍的6个程序替换库函数底层都会调用execve。所以这六个库函数只是传参形式不同,设计这六个库函数的目的是为了满足未来不同场景的需求。
在这里插入图片描述

子进程执行用户写的程序

子进程不仅可以替换系统命令,也可以通过程序替换执行我们自己写的程序,只要能找到就行了,示例如下,让子程序执行我们自己用C++写的mycmd程序:
mycmd.cc
#include<iostream>intmain(){ std::cout <<"hello C++"<< std::endl;return0;}
myexec.c:
#include<stdio.h>#include<unistd.h>#include<stdlib.h>#include<sys/types.h>#include<sys/wait.h>intmain(){ pid_t id =fork();if(id ==0){//子进程sleep(2);printf("我是一个进程:%d\n",getpid());sleep(1);execl("./mycmd","./mycmd",NULL);exit(1);}//父进程 pid_t rid =waitpid(id,NULL,0);if(rid >0){printf("wait: %d success\n", rid);}return0;}
运行结果:
在这里插入图片描述

程序替换可以调用任意语言的程序

有了上面用C语言替换C++程序的铺垫后,小编想说不论是什么语言编写的代码运行后都会变成进程,而程序替换其实就是替换进程,所以我们可以用C语言写的程序替换代码调用其他语言运行起来的程序。
程序替换是操作系统的功能,所以不止C语言,任何语言编写的程序都能完成程序替换。

传递命令行参数和环境变量的2种方式

1、有了上面关于程序替换的认识,我们学习到了命令行参数和环境变量有两种传递方式,第一种方式是通过程序替换接口(exec**e)的方式将当前程序的环境变量或者其他任意环境变量灵活传递给替换该当前程序的程序,所有exec函数都会把命令行参数传递给替换后的程序。
2、第二种方式是父子进程之间通过虚拟地址空间传递,因为在父进程的进程地址空间中会存在它的命令行参数和环境变量,就如同全局变量一样,子进程会继承到父进程的进程虚拟空间和页表,自然子进程也能拿到命令行参数和环境变量。

以上就是小编分享的全部内容了,如果觉得不错还请留下免费的关注和收藏
如果有建议欢迎通过评论区或私信留言,感谢您的大力支持。
一键三连好运连连哦~~

在这里插入图片描述

Read more

SpringBoot 整合 Langchain4j 实现会话记忆存储深度解析

SpringBoot 整合 Langchain4j 实现会话记忆存储深度解析

目录 一、前言 二、AI大模型会话记忆介绍 2.1 AI 大模型的会话记忆是什么 2.2 AI 大模型为什么需要会话记忆 2.3 AI 大模型会话记忆常用实现方案 2.4 LangChain4j 会话记忆介绍 2.4.1 LangChain4j 会话记忆介绍 2.4.2 LangChain4j 会话记忆类型 三、Langchain4j 会话记忆操作案例使用 3.1 前置准备 3.1.1 导入依赖文件 3.1.2 添加配置文件 3.1.3 前置案例 3.

By Ne0inhk

LIBWEBKIT2GTK-4.1-0实战:构建一个轻量级浏览器

快速体验 1. 打开 InsCode(快马)平台 https://www.inscode.net 2. 输入框内输入如下内容: 开发一个轻量级浏览器应用,基于LIBWEBKIT2GTK-4.1-0,支持多标签页、书签管理和基本的导航功能。应用应包含一个简洁的UI,允许用户输入URL并显示网页内容。使用DeepSeek模型生成核心渲染代码,并确保应用在Linux环境下流畅运行。 1. 点击'项目生成'按钮,等待项目生成完整后预览效果 最近在折腾一个轻量级浏览器的开发项目,用到了LIBWEBKIT2GTK-4.1-0这个库,发现它真是个宝藏工具。今天就把整个实战过程整理成笔记,分享给同样对浏览器开发感兴趣的小伙伴们。 1. 环境准备与基础搭建 首先得确保系统安装了LIBWEBKIT2GTK-4.1-0库。在Ubuntu/Debian系系统里,一条简单的apt命令就能搞定。这个库基于WebKit引擎,提供了GTK+的接口封装,特别适合用来开发轻量级的图形界面浏览器。 1. 创建基础窗口结构 用GTK+创建主窗口时,需要设置好标题、

By Ne0inhk
手把手教你做一个非遗守艺人网站——Web课程作业完整实战

手把手教你做一个非遗守艺人网站——Web课程作业完整实战

最近在做Web开发技术的课程作业,老师要求做一个完整的网站,包括主页和至少3个子页面。我选的主题是"我的家乡非遗守艺人",今天就把整个制作过程记录下来,希望能帮到也在做作业的同学。 项目预览 先看看最终效果吧,这是我做出来的网站首页: 整个网站包含: * 首页(带轮播图和3D翻转卡片) * 守艺名录页面(可以分类筛选) * 传统工艺页面(时间轴展示) * 传承故事页面(文章列表) * 加入我们页面(表单收集信息) 技术栈:HTML5 + CSS3 + JavaScript + jQuery(纯前端,不需要后端) 第一步:搭建项目结构 最开始我也是一头雾水,不知道从哪里下手。后来想明白了,先搭好框架,后面慢慢填充内容就行。 创建文件夹 在电脑上新建一个文件夹,名字叫 html非遗守艺人模板1(你也可以用其他名字)。然后在这个文件夹里创建三个子文件夹: html非遗守艺人模板1/ ├── css/ # 放样式文件 ├── js/ # 放JavaScript文件 └── img/ # 放图片

By Ne0inhk
实验室管理系统信息管理系统源码-SpringBoot后端+Vue前端+MySQL【可直接运行】

实验室管理系统信息管理系统源码-SpringBoot后端+Vue前端+MySQL【可直接运行】

摘要 随着信息技术的快速发展,实验室管理系统的智能化与信息化成为提升科研效率和管理水平的关键。传统实验室管理依赖人工记录和纸质文档,存在数据易丢失、查询效率低、资源共享困难等问题。实验室信息管理系统(LIMS)通过数字化手段整合实验室资源,优化实验流程,实现数据的实时监控与高效管理。该系统能够满足实验室在设备管理、人员调度、实验数据存储及分析等方面的需求,显著提升实验室的运营效率和数据安全性。关键词:实验室管理、信息化、数据安全、效率提升、资源共享。 本系统采用前后端分离架构,后端基于SpringBoot框架实现,提供RESTful API接口,支持高并发和分布式部署;前端使用Vue.js框架,结合Element UI组件库,实现动态响应和友好的用户交互。数据库采用MySQL,通过JPA实现对象关系映射,确保数据的完整性和高效查询。系统核心功能包括用户权限管理、实验设备预约、实验数据上传与分析、报表生成等。通过多角色权限控制,系统能够适应管理员、教师、学生等不同用户的需求,实现实验室资源的合理分配与高效利用。关键词:SpringBoot、Vue.js、MySQL、权限管理、

By Ne0inhk