(学习总结31)Linux 进程地址空间与进程控制
Linux 进程地址空间与进程控制
- 进程地址空间
- Linux 进程地址空间分布
- 虚拟地址空间和页表
- 创建子进程与写时拷贝
- 权限访问
- 缺页中断
- Linux 虚拟内存管理
- mm_struct 内存描述符
- 区域划分
- vm_area_struct 独立虚拟内存区域
- 虚拟地址空间的作用
- 进程控制
- 进程创建
- fork 函数介绍
- 写时拷贝作用
- 进程终止
- 进程退出
- 退出码
- _exit、exit 函数 与 缓冲区
- return 退出
- 进程等待
- 进程等待作用
- 进程等待的方法
- wait 方法
- waitpid 方法
- 获取子进程 wstatus
- 阻塞与非阻塞等待
- 进程程序替换
- 替换原理
- 替换函数
- execl 函数使用
- execv 函数使用
- execlp 和 execvp 函数第一个参数在 PATH 环境变量中查找
- execle 与 execvpe 函数增加环境变量参数
- exec 函数系列命名理解
- exec 函数系列替换其它语言程序
以下代码环境默认为 Linux Ubuntu 22.04.5 gcc C语言。
进程地址空间
Linux 进程地址空间分布
Linux 内核 2.6.32 版本 32 位平台,进程的地址空间大致分布图为:
在 (学习总结29)Linux 进程概念和进程状态 - 进程概念 中的父子进程写时拷贝演示中我们知道,进程的地址不是物理地址,而是虚拟地址。
这里来段代码测试一下具体的地址分布:
#include <stdio.h>
#include <stdlib.h>int g_num2;
int g_num1 = 100;int main(int argc, char* argv[], char* env[])
{const char* pStr = "Linux";printf("%p : 代码区\n", &main);printf("%p : 字符常量区\n", pStr);printf("%p : 初始化的全局区\n", &g_num1);printf("%p : 未初始化的全局区\n", &g_num2);static int num4; static int num3 = 0;printf("%p : 局部变量静态区\n", &num3);printf("%p : 局部变量静态区\n", &num4);char* ptr1 = (char*)malloc(sizeof(char) * 5); int* ptr2 = (int*)malloc(sizeof(int) * 5); printf("%p : 堆区\n", ptr1);printf("%p : 堆区\n", ptr2);int num1 = 0;int num2;int* ptr_int = NULL;printf("%p : 栈区\n", &num1);printf("%p : 栈区\n", &num2);printf("%p : 栈区\n", &ptr_int);for (int i = 0; i < argc; ++i){ printf("%p : 命令行参数区 argv[%d]\n", argv[i], i); } for (int i = 0; i < 5; ++i){ printf("%p : 环境变量区 env[%d]\n", env[i], i); } return 0;
}
事实上,我们从 C/C++ 语言所看到的地址,都是虚拟地址。物理地址用户看不到,由操作系统统一管理,而操作系统必须负责将 虚拟地址 转化成 物理地址。
虚拟地址空间和页表
进程创建时,会有一个虚拟地址空间和一套页表,而页表是用来做虚拟地址和物理地址映射的:
创建子进程与写时拷贝
使用 fork 函数创建子进程时,子进程会拷贝父进程的虚拟地址和页表,但为节约内存空间不会拷贝具体的数据,此时两者的虚拟地址就能通过一样的页表指向同一个资源。若子进程修改变量,操作系统会修改子进程的页表虚拟地址与物理地址的映射,将修改的变量数据放在另一个物理地址,正确映射关系:
这也是写时拷贝的基本原理。
权限访问
页表也不止映射功能,还可以查询操作具体数据的权限,若数据具有常量属性不可更改(如常量字符串),在访问时就会拒绝操作:
缺页中断
当内存空间不足,操作系统会将没有使用的进程代码和数据,临时的放入磁盘交换分区,对于新进程,不会将进程代码和数据加载到内存。此时页表对应的物理地址映射未知,并且访问资源时操作系统会检查是否加载到实际内存中。若操作的资源没有加载,操作系统会进行页表物理映射与加载资源:
Linux 虚拟内存管理
进程的虚拟地址空间和页表也会被操作系统管理起来,这就是所谓的虚拟内存管理。
mm_struct 内存描述符
描述 Linux 进程地址空间所有信息的结构体是 mm_struct(内存描述符)。每个进程只有一个 mm_struct 结构,在每个进程的 task_struct 结构中,有一个指向 mm_struct 的指针(Linux 内核 2.6.32):
可以说,mm_struct 结构是对整个用户空间的描述。每一个进程都会有自己独立的 mm_struct。
区域划分
mm_struct 还会对每个区域进行划分,只需要使用变量记录对应区域开始地址(start) 和结束地址(end) ,扩充或缩小区域使用加减即可:
vm_area_struct 独立虚拟内存区域
有些区域开辟空间并不是连续的。如堆空间每次申请并不是连续的,而是这里一堆那里一堆,如何记录呢?Linux 使用了 vm_area_struct 独立虚拟内存区域:
虚拟空间的组织方式有两种:
-
当虚拟区间较少时采取单链表,由 mmap 指针指向这个链表。
-
当虚拟区间较多时采取红黑树进行管理,由 mm_rb 指向这棵树。
Linux 内核使用 vm_area_struct 结构来表示一个独立的虚拟内存区域(Virtual Memory Area),由于每个不同的虚拟内存区域功能和内部机制都不同,因此一个进程使用多个 vm_area_struct 结构来分别表示不同类型的虚拟内存区域。上面提到的两种组织方式使用的就是 vm_area_struct 结构来连接各个 VMA,方便进程快速访问。
所以我们可以对之前的虚拟地址空间图再进行更细致的描述,如下图所示:
虚拟地址空间的作用
在早期的计算机中,要运行一个程序,会把这些程序全都装入内存,程序都是直接运行在内存上的,也就是说程序中访问的内存地址都是实际的物理内存地址。当计算机同时运行多个程序时,必须保证这些程序用到的内存总量要小于计算机实际物理内存的大小。
这种简单的内存分配策略问题很多:
-
安全风险:每个进程都可以访问任意的内存空间,意味着任意一个进程都能够去读写系统相关内存区域,中间没有检查管理机制。如果是一个木马病毒,那么它就能随意的修改内存空间,让设备直接瘫痪。
-
内存管理复杂化:程序员需要处理物理内存布局,在连续的地址空间中寻找并申请内存。
-
物理内存碎片:物理地址空间中的离散区域会出现碎片问题,降低了内存的利用率。
-
效率低下:直接使用物理内存,一个进程就是作为一个整体(内存块)操作的,如果出现物理内存不够用的时候,一般的办法是将不常用的进程拷贝到磁盘的交换分区中,好腾出内存。但如果是物理地址的话,则要将整个进程一起拷走,这样在内存和磁盘之间拷贝时间太长,效率较低。
若有了虚拟地址空间和分页机制就能解决上面的问题:
-
安全维护:地址空间和页表是操作系统创建并维护的,使用内存时,也一定会在操作系统的监管之下来进行访问,保护了物理内存中的所有合法数据。
-
简化内存管理与高效:在 C/C++ 语言上 new,malloc 空间时,其实是在地址空间上申请的,物理内存操作系统可以选择不申请。而当程序员真正进行对物理地址空间访问的时候,才执行内存的相关管理算法,来申请内存,构建页表映射关系(延迟分配),此过程全程由操作系统完成。
-
功能解耦合:因为有虚拟地址空间和页表的映射,物理内存中可以对未来的数据进行任意位置的加载,物理内存分配 和 进程管理 就可以做到模块分离,完成解耦合。
-
避免物理内存碎片:因为页表映射的存在,程序理论上就可以任意位置加载。它可以将地址空间上的虚拟地址和物理地址进行映射,在进程视角所有的内存分布都可以是有序的。
-
扩展可用内存:通过将不活跃的页面暂存至磁盘交换空间,虚拟地址空间可远大于物理内存,支持运行更大程序或多任务并行(时间换空间)。并且程序启动时可以仅加载必要部分,其余内容(如未执行代码)在访问时按需载入,节省物理内存。
进程控制
进程创建
fork 函数介绍
在 Linux 中,fork 函数是非常重要的函数,我们在 (学习总结29)Linux 进程概念和进程状态 曾具体使用过它,实践代码部分省略。
fork 函数作用是从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
进程调用 fork 函数,当执行到内核中的 fork 函数代码后,内核将会做:
-
分配内核数据结构给子进程
-
将父进程部分数据结构内容拷贝到子进程
-
父子进程的页表指向相同的物理内存,但标记所有内存页为只读
-
添加子进程到系统进程调度列表当中
-
fork 函数返回,开始调度器调度
当一个进程调用 fork 后,会出现两个二进制代码相同的进程。而且它们都运行到相同的代码命令。但每个进程都是独立操作它们自己的数据。
当父进程或子进程尝试修改内存数据时,触发缺页异常(上一部分讲述的进程地址空间的权限访问与写时拷贝),内核复制数据到其它地方并修改页表项为私有可写。
fork 之前父进程独立执行,之后父子两个执行流分别执行。注意 fork 之后谁先执行完全由调度器决定。
fork 函数返回值中,子进程返回 0,父进程返回的是子进程的 pid(pid > 0)。
fork 函数常规用法:
-
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如:父进程等待客户端请求,生成子进程来处理请求。
-
一个进程要执行一个不同的程序。例如:子进程从 fork 函数返回后,调用 exec 函数。
fork 调用失败的原因:
-
系统中有进程过多
-
实际用户的进程数超过规定限制
写时拷贝作用
我们在其它部分提到或讲解过写时拷贝,但它的具体作用是什么呢?
-
减少 fork 函数开销,加快进程创建速度。
-
延时申请思想,其避免立即复制全部内存,提升性能并提高整机内存的使用率(尤其适用于 fork 函数后使用 exec 函数的场景)
进程终止
进程终止的本质是释放系统资源,即释放进程申请的相关 内核数据结构
和 对应代码与数据
。
进程退出
进程退出的情况有 3 种:
-
代码运行完毕,结果正确
-
代码运行完毕,结果不正确
-
代码异常终止
进程常见正常退出方法:
-
从 main 函数返回
-
调用 exit 函数返回
-
调用 _exit 函数返回
正常退出时可以用 echo $?
命令查看进程退出码。
当然还有异常退出(以下只是一部分):
-
Ctrl + c 快捷键退出
-
除数为 0 错误
-
访问野指针
注意,异常退出时的退出码获取了没有意义。
退出码
退出码(退出状态)可以告诉我们进程执行的状态。在进程结束以后,我们可以知道此进程的执行是成功结束还是以错误结束的。其基本思想是,进程返回退出代码为 0 时表示执行成功,没有问题。
为 0 以外的任何代码都被视为不成功。
Linux shell 中的主要退出码:
-
退出码 0 表示进程执行无误,这是完成进程任务的理想状态。
-
退出码 1 为 “ 不被允许的操作 ” 。如在没有 sudo 情况使用需要 root 权限的命令,还有除数为 0 等操作也会返回。
-
C语言规定的退出码一共有 134 个(0 ~ 133)。
-
可以使用C语言的 strerror 函数来获取退出码对应的描述。
-
130 ( SIGINT 或 ^C )和 143 ( SIGTERM )等终止信号是非常典型的,它们属于 128 + n 信号,其中 n 代表终止码,使用后也能看到退出码。
我们可以使用 strerror 函数来查看具体的退出码:
#include <stdio.h>
#include <string.h>int main()
{for (int i = 0; i < 135; ++i){printf("str[%d] == %s\n", i, strerror(i)); }return 0;
}
_exit、exit 函数 与 缓冲区
_exit 函数是系统层面,而 exit 属于C语言标准库的,我们可以使用 man 手册查看:
exit 是 C语言 的库函数,内部会清理C语言开辟的缓冲区,最后才会调用 _exit。但在调用 _exit 之前,还做了其它工作:
-
执行用户通过 atexit 或 on_exit 定义的清理函数。
-
关闭所有打开的流,所有的缓存数据均被写入
-
调用 _exit
_exit 和 exit 对缓冲区执行的区别:
C语言打印时,并不是一个一个的打印在控制台上,而是遇到 \n
等规定的字符才会打印,在此之前字符将会暂时存储在C语言的缓冲区中。
如果没有 \n
但使用 exit 时,exit 会关闭缓冲区,将其中的字符打印出来。但若是调用 _exit ,_exit 会直接让进程退出,缓冲区直接杀掉,自然看不到打印的字符:
#include <stdio.h>
#include <stdlib.h>int main()
{printf("Hello Linux"); // 没有 \n 刷新缓冲区exit(0); // 使用 exit 会刷新缓冲区并打印其中的字符 return 0;
}
#include <stdio.h>
#include <unistd.h>int main()
{printf("Hello Linux"); _exit(0); // 使用 _exit 会杀掉缓冲区,不会打印其中的字符return 0;
}
status 的相关规定:
虽然 _exit 和 exit 中的 status 是 int,但是仅有低 8 位可以被父进程所用。所以 _exit(-1) 或 exit(-1) 时,在终端执行 echo $?
发现返回值是 255 :
#include <stdio.h>
#include <stdlib.h> int main()
{exit(-1);return 0;
}
return 退出
return 是一种更常见的退出进程方法。在 main 中执行 return n 等同于执行 exit(n),因为调用 main 的运行时函数会将 main 的返回值当做 exit 的参数。
进程等待
进程等待作用
父进程通过进程等待的方式:
-
回收子进程资源
-
获取子进程退出信息以便后续操作
所以出现了进程的僵尸状态概念,而僵尸进程是无法被消灭的,即便使用 kill -9 [进程PID]
具有最高的优先级和不可抗拒性的命令也无能为力,因为谁也没有办法杀死一个已经死去的进程。
我们在 (学习总结29)Linux 进程概念和进程状态 - 进程状态 中介绍过僵尸进程的危害。子进程退出,父进程如果不管不顾,会造成 " 僵尸进程 " 的问题,进而造成内存泄漏。
可以在 task_struct 中找到僵尸进程的退出信息:
进程等待的方法
我们可以使用 man 手册查看 wait 和 waitpid 函数:
wait 方法
返回值 pid_t :成功返回被等待进程 pid,失败返回 -1。
参数 wstatus:输出型参数,获取子进程退出状态,不关心则可以设置成为 NULL。
wait 函数等待任意一个子进程,但它会让父进程进入阻塞状态,如果子进程一直没有结束,父进程就会一直等待:
#include <stdio.h>
#include <sys/wait.h>
#include <unistd.h>int main()
{pid_t pid = fork();if (pid < 0){ perror("fork:");return 0;} else if (pid == 0){ printf("child process: %d\n", getpid());sleep(2); // 子进程停止 2 秒返回} else{ pid_t ret = wait(NULL);if (ret == -1) { return 0;} else{ printf("get PID: %d\n", ret);} } return 0;
}
waitpid 方法
返回值 pid_t :
-
成功返回被等待进程 pid,失败返回 -1。
-
如果参数 options 设置了选项 WNOHANG,而调用中 waitpid 发现没有符合要求的子进程可收集,则返回 0。
参数 wstatus:输出型参数,获取子进程退出状态,不关心则可以设置成为 NULL。
waitpid 参数中的 pid 表示的是子进程的 PID ,具体填写的操作:
参数 options 等待方式,0 表示阻塞等待,其它等待方式:
WNOHANG:若 pid 指定的子进程没有结束,则 waitpid() 函数返回 0,不予以等待。若正常结束,则返回该子进程的 ID。
waitpid 功能相对于 wait 更多,特别是 WNOHANG 无阻塞调用,在下一部分我们再具体分析。
获取子进程 wstatus
wait 和 waitpid,都有一个 wstatus 参数,该参数是一个输出型参数,由操作系统填充。如果传递 NULL,表示不关心子进程的退出状态信息,反之操作系统会根据该参数,将子进程的退出信息反馈给父进程。
但是 wstatus 不能简单的当作整形来看待,要当作位图来看待,具体细节如下图(这里只分析 wstatus 低 16 比特位):
我们可以测试正常退出:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>int main()
{pid_t pid = fork();if (pid < 0){ perror("fork:");return 0;} else if (pid == 0){ printf("child process: %d\n", getpid());sleep(5);exit(6);} else{ int status = 0;pid_t ret = waitpid(-1, &status, 0); if (ret == -1) { return 0;} else{ printf("子进程 PID: %d\n", ret);printf("子进程退出码: %d , 终止信号: %d\n", (status >> 8) & 0xFF, status & 0x7F);} } return 0;
}
这里再测试异常退出:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>int main()
{pid_t pid = fork();if (pid < 0){ perror("fork:");return 0;} else if (pid == 0){ printf("child process: %d\n", getpid());int num = 5 / 0; // 除数为 0 异常 exit(6);} else{ int status = 0;pid_t ret = waitpid(-1, &status, 0); if (ret == -1) { return 0;} else{ printf("子进程 PID: %d\n", ret);printf("子进程退出码: %d , 终止信号: %d\n", (status >> 8) & 0xFF, status & 0x7F);} } return 0;
}
注意前面已经提醒过了,异常退出中的退出码是无意义的:
对于提取 wstatus 的操作也可以使用宏:
WIFEXITED(wstatus)
:若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
WEXITSTATUS(wstatus)
:若 WIFEXITED 为真,提取子进程退出码。(查看进程的退出码)
WIFSIGNALED(wstatus)
:检查子进程是否因信号而终止(非正常退出),为真说明子进程是被信号杀死的。
WTERMSIG(wstatus)
:如果 WIFSIGNALED 为真,提取导致子进程终止的信号编号(查看进程的终止信号码)
如果想看看有哪些信号,我们可以使用 kill -l
查看所有信号:
可以注意到刚刚的除以 0 操作为 8 号信号 SIGFPE
算术运算异常,另外 kill -9 [进程PID]
表示为杀掉进程的 9号信号 SIGKILL
,Ctrl + c 快捷键为 2号信号 SIGINT
。
阻塞与非阻塞等待
从之前文章的分析当中,阻塞等待就是将进程从调度队列转移到对应的等待队列,如果等待队列没有获取对应资源,就会一直等待。
而刚刚我们提到了 waitpid 的无阻塞 WNOHANG 模式。无阻塞最大的用途在于不用一直等待,可以在等待的时间内去做其它事情。
这意味着父进程可以在检查后去做自己的事情,大大提高处理任务的效率:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdbool.h>void test1()
{printf("执行打印任务\n");
}void test2()
{printf("执行计算任务\n");
}typedef void (*func)();int main()
{func task[2] = { &test1, &test2 };pid_t pid = fork();if (pid < 0){perror("fork:");return 0;}else if (pid == 0) {sleep(10);}else{int status = 0;int task_move = 0;int ret = 0;while (true){ret = waitpid(-1, &status, WNOHANG);if (ret != 0){break;} else{printf("未等待到子进程,执行其它任务\n");}task[task_move++ % 2]();sleep(2);}if (ret < 0){return 0;}printf("子进程 PID: %d\n", ret);printf("子进程退出码: %d , 终止信号: %d\n", (status >> 8) & 0xFF, status & 0x7F);}return 0;
}
进程程序替换
fork 函数调用完成之后,父子各自执行父进程代码的一部分,如果让子进程执行一个全新的程序可以用进程程序替换来完成这个功能。
程序替换是通过特定的接口,加载磁盘上的一个全新的程序(代码和数据),加载到调用进程的地址空间中。
替换原理
用 fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程需要调用一种 exec 函数才能执行另一个程序。当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序覆盖式的替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 id 并未改变:
替换函数
一般有六种以 exec 开头的函数,统称 exec 系列函数,除了 execvpe 函数其它属于 POSIX 标准函数,exec 系列函数的共同目标是用新进程替换当前进程,但它们在参数传递、环境变量处理和可执行文件查找方式上有所不同:
execl 函数使用
execl 函数如果调用成功则加载新的程序并从启动代码开始执行,不再返回。如果调用出错则返回 -1,则 execl 函数只有出错的返回值而没有成功的返回值。
第一个参数表示查找的可执行文件,第二个参数与后面的表示怎样执行它,而且参数可变,用 NULL 表示结束:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>int main()
{pid_t id = fork();if (id < 0){ perror("fork:");return 1;} else if (id == 0){ printf("子进程 PID: %d\n", getpid());int ret = execl("/usr/bin/ls", "ls", "-l", "-a", NULL); // 以 NULL 结尾printf("子进程执行\n");if (ret == -1) { exit(1);} } else{ sleep(1); printf("父进程执行\n");pid_t child = wait(NULL);if (child > 0){ printf("子进程 PID: %d\n", child);} } return 0;
}
其它 5 个函数返回值同 execl 一样,但参数部分各有差异。
execv 函数使用
execv 函数:相对于 execl 函数,execv 中的 v
字符表示传的第二个与后面参数变为字符指针数组。
在 execl 函数的基础上只需改部分代码:
//......char* const args[] = { (char* const)"ls",(char* const)"-l",(char* const)"-a",NULL}; int ret = execv("/usr/bin/ls", args);//......
execlp 和 execvp 函数第一个参数在 PATH 环境变量中查找
execlp 函数 :在 execl 函数名上加了字符 p
,表示第一个参数会在 PATH 环境变量中查找。
在 execl 函数的基础上只需修改部分代码:
// ......int ret = execlp("ls", "ls", "-l", "-a", NULL);// ......
execvp 函数:在 execv 函数名上加了字符 p
,表示第一个参数会在 PATH 环境变量中查找。
在 execv 函数的基础上只需改部分代码:
// ......int ret = execvp("ls", args);// ......
execle 与 execvpe 函数增加环境变量参数
execle 函数:在 execl 函数名上加了 e
字符,表示后面增加环境变量参数。
execvpe 函数:在 execvp 函数上加了 e
字符,表示后面增加环境变量参数。
如果想要保留之前的环境参数,可以声明 extern char ** environ;
并在参数上传递:
其它部分代码省略:
// execle 部分 =========================
// .......extern char** environ;// ......int ret = execle("/usr/bin/ls", "ls", "-l", "-a", NULL, environ);// ......// execvpe 部分 =========================// ......extern char** environ;// ......char* const args[] = { (char* const)"ls",(char* const)"-l",(char* const)"-a",NULL}; //char* const env[] = {// (char* const)"my_env=12345"//};int ret = execvpe("ls", args, environ);// ......
exec 函数系列命名理解
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
-
l(list):表示参数采用列表
-
v(vector):表示参数用数组
-
p(path):有字符
p
表示自动搜索环境变量 PATH 里的可执行文件 -
e(env):表示更改或增加维护的环境变量
但要补充的是,只有 execve 是真正的系统调用,exec 函数中五个函数统一都会调用 Linux 系统提供的 execve 函数:
如果函数名没带 e
字符,最后调用 execve 将继承当前进程的环境变量。
所以 execve 在 man 手册第 2 节,其它函数在 man 手册第 3 节。
那 execvpe 函数呢?事实上 execvpe 函数并非 POSIX 标准函数,而是 GNU C 库(glibc)提供的扩展函数。它的存在取决于具体实现和系统环境。
我们可以用表格来具体对比它们:
函数名 | 参数格式 | 使用 PATH 查找 | 传递环境变量 | 标准 |
---|---|---|---|---|
execl | 列表 | 否 | 继承当前环境 | POSIX |
execv | 数组 | 否 | 继承当前环境 | POSIX |
execlp | 列表 | 是 | 继承当前环境 | POSIX |
execvp | 数组 | 是 | 继承当前环境 | POSIX |
execle | 列表 | 否 | 需显式传递 | POSIX |
execve | 数组 | 否 | 需显式传递 | POSIX |
execvpe | 数组 | 是 | 需显式传递 | GNU 扩展 |
exec 函数系列替换其它语言程序
exec 函数不仅可以替换C语言程序,还可以替换其它语言的程序!因为不管什么语言,如:C++、Java、Python 等等,最后程序运行都是用进程的方式。
这里我们编写一个 C++ 的程序来测试:
#include <iostream>
using namespace std;int main(int argc, char* argv[], char* env[])
{cout << "运行 C++ 程序\n"; for (int i = 0; i < argc; ++i){ cout << "参数[" << i << "] == " << argv[i] << endl;} for (int i = 0; i < 10; ++i){ cout << "环境变量[" << i << "] == " << env[i] << endl;} return 0;
}
C语言程序准备好调用 C++ 程序:
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>extern char** environ;int main()
{ pid_t id = fork();if (id < 0){ perror("fork:");return 1;} else if (id == 0){ printf("子进程 PID: %d\n", getpid());int ret = execle("./testCpp", "testCpp", "-a", "-l", NULL, environ);printf("子进程执行\n");if (ret == -1) { exit(1);} } else{ sleep(1);printf("父进程执行\n");pid_t child = wait(NULL);if (child > 0){ printf("子进程 PID: %d\n", child);} } return 0;
}
结果:
相关文章:
(学习总结31)Linux 进程地址空间与进程控制
Linux 进程地址空间与进程控制 进程地址空间Linux 进程地址空间分布虚拟地址空间和页表创建子进程与写时拷贝权限访问缺页中断 Linux 虚拟内存管理mm_struct 内存描述符区域划分vm_area_struct 独立虚拟内存区域 虚拟地址空间的作用 进程控制进程创建fork 函数介绍写时拷贝作用…...
基于Springboot的网上订餐系统 【源码】+【PPT】+【开题报告】+【论文】
网上订餐系统是一个基于Java语言和Spring Boot框架开发的Web应用,旨在为用户和管理员提供一个便捷的订餐平台。该系统通过简化餐饮订购和管理流程,为用户提供快速、高效的在线订餐体验,同时也为管理员提供完善的后台管理功能,帮助…...
Axure设计之中继器表格——拖动行排序教程(中继器)
一、原理介绍 在Axure中实现表格行的拖动排序,主要依赖于中继器的排序事件。然而要实现拖动效果,就必须结合动态面板,因为动态面板可以设置拖动事件,之所以使用动态面板或许是因为它可以更灵活地处理位置变化。用户拖动行时&…...
GAUSSDB 分布式存储机制深度解析
GAUSSDB 分布式存储机制深度解析 一、核心机制概览 GAUSSDB 的分布式存储通过 数据分片、多副本管理 和 全局事务控制 实现高可用与水平扩展。以下为关键机制详解: 二、数据分片(Sharding) 1. 分片策略 GAUSSDB 支持多种分片规则ÿ…...
【NLP 46、大模型技术发展】
目录 一、ELMo 2018 训练目标 二、GPT-1 2018 训练目标 三、BERT 2018 训练目标 四、Ernie —— baidu 2019 五、Ernie —— Tsinghua 2019 六、GPT-2 2019 七、UNILM 2019 八、Transformer - XL & XLNet 2019 1.模型结构 Ⅰ、循环机制 Recurrence Mechanism Ⅱ、相对位置…...
汽车加气站操作工证书报考条件是什么?
关于汽车加气站操作工的资格证书: 一、核心证书要求 CNG充装人员上岗证 这是加气站加气工的核心资质证书,需通过专业培训并考核。该证书由相关部门颁发,证明持证人具备从事CNG(压缩天然气)充装操作的专业技能…...
材质及制作笔记
基本流程: 建中模——zb雕刻高模——maya拓扑低模——拆uv——sp烘焙贴图——sp绘制材质——渲染 1 材质贴图: diffuse/albedo/basecolor:漫反射 reflection/specular:反射 metalness:金属度 glossiness…...
Opencv 图像读取与保存问题
本文仅对 Opencv图像读取与保存进行阐述,重在探讨图像读取与保存过程中应注意的细节问题。 1 图像读取 首先看一下,imread函数的声明: // C: Mat based Mat imread(const string& filename, int flags1 );// C: IplImage based IplImage*…...
Flutter 2025生态全景:从跨端到嵌入式开发的新机遇
一、技术演进:从"一次编写多端运行"到"全场景覆盖" 1.1 渲染引擎革命:Impeller 2.0的性能突破 // 启用Impeller的配置示例(android/app/build.gradle) def enableImpeller true android {defaultConfig {…...
Idea中诡异的文件编码问题: 设置fileCodeing为UTF8但不生效
在fileCoding配置了编码utf-8,右下角的文件里编码格式却是ISO-8859-1,git后到远程仓库里却是ISO-8859-1的乱码,怎么修改都不生效,重启也不行,恶心的不行。 最后发现修复方案是: 1. 先随便做个变更&#x…...
Linux中逻辑卷的使用、扩容与磁盘配额
目录 一.逻辑卷概述 1.普通分区的优缺点 2.逻辑卷的优点 3.逻辑卷概述 二.逻辑卷的创建 1.逻辑卷相关命令 2.创建步骤 三.逻辑卷的扩容 1.扩容步骤 四.磁盘配额 1.概念及注意点 2.使用条件 3.相关命令 一.逻辑卷概述 1.普通分区的优缺点 普通分区优点:…...
Java版Manus实现来了,Spring AI Alibaba发布开源OpenManus实现
此次官方发布的 Spring AI Alibaba OpenManus 实现,包含完整的多智能体任务规划、思考与执行流程,可以让开发者体验 Java 版本的多智能体效果。它能够根据用户的问题进行分析,操作浏览器,执行代码等来完成复杂任务等。 项目源码及…...
关于IP免实名的那些事
IP技术已成为个人与企业保护隐私、提升网络效率的重要工具。其核心原理是通过中介服务器转发用户请求,隐藏真实IP地址,从而实现匿名访问、突破地域限制等目标。而“免实名”代理IP的出现,进一步简化了使用流程,用户无需提交身份信…...
从零构建大语言模型全栈开发指南:第二部分:模型架构设计与实现-2.2.3实战案例:在笔记本电脑上运行轻量级LLM
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 实战案例:在笔记本电脑上运行轻量级LLM2.2.3 模型架构设计与实现1. 环境与工具准备1.1 硬件要求1.2 软件栈选择2. 轻量级模型架构设计2.1 模型参数配置2.2 关键技术优化3. 实战流程3.1 数据准备流程3.2…...
Labview信号分析系统(含报告)
1.摘要 本文详细介绍了基于LabVIEW的轴承故障信号分析系统的设计与实现。该系统以凯斯西储大学轴承驱动端的振动信号为研究对象,涵盖了正常信号以及内圈、滚珠、外圈三种故障信号的分析。系统可以对信号进行滤波、时域分析、频域分析和时频域分析等。系统的功能如下…...
基于FastAPI与Kimi AI的智能聊天应用开发实践
一、项目概述 本文介绍一个基于现代Web技术栈的智能对话系统,前端采用响应式设计实现聊天界面,后端通过FastAPI框架构建高性能API服务,集成Moonshot AI大语言模型实现智能对话功能。系统具备完整的消息交互流程,支持参数定制化配…...
Android 蓝牙/Wi-Fi通信协议之:经典蓝牙(BT 2.1/3.0+)介绍
在 Android 开发中,经典蓝牙(BT 2.1/3.0)支持多种协议,其中 RFCOMM/SPP(串口通信)、A2DP(音频流传输)和 HFP(免提通话)是最常用的。以下是它们在 Android 中的…...
介绍FRAMES:一个统一的检索增强生成评估框架
引言 大型语言模型(LLMs)在认知任务上取得了显著进步,检索增强生成(RAG)技术成为提升模型性能的重要方法。然而,现有的评估方法往往孤立地测试模型的检索能力、事实性和推理能力,无法全面反映模…...
云钥科技多通道工业相机解决方案设计
项目应用场景分析与需求挑战 1. 应用场景 目标领域:工业自动化检测(如精密零件尺寸测量、表面缺陷检测)、3D立体视觉(如物体建模、位姿识别)、动态运动追踪(如高速生产线监控)等。 核心…...
微信小程序逆向开发
一.wxapkg文件 如何查看微信小程序包文件: 回退一级 点击进入这个目录 这个就是我们小程序对应的文件 .wxapkg概述 .wxapkg是微信小程序的包文件格式,且其具有独特的结构和加密方式。它不仅包含了小程序的源代码,还包括了图像和其他资源文…...
Debian ubuntu源
配置文件路径 /etc/apt/sources.list 阿里 deb http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs) main restricted universe multiverse deb-src http://mirrors.aliyun.com/ubuntu/ $(lsb_release -cs) main restricted universe multiverse deb http://mirrors.aliyun.…...
几个工作中常用的网站
1.思维导图流程图 在线版 ProcessOn思维导图流程图-在线画思维导图流程图_在线作图实时协作 2.编码转换及报文格式化 ESJSON在线工具首页(ES JSON在线工具) JSON在线解析格式化验证 - JSON.cn 3.AI助手 通义 - 你的个人AI助手 文心一言 谷灵AI,你的工作好帮手…...
《Apipost如何超越Postman与Apifox?SocketIO调试工具横评》
实时通信调试工具可能已「过时」!许多主流工具如Apifox不支持SocketIO协议,导致调试效率低下。而Apipost作为国产工具,原生支持SocketIO,提供自定义事件管理、连接状态可视化等功能,极大提升了实时通信开发的效率和准确…...
为什么要将项目部署到外部tomcat
一、是什么 指将你的Java Web应用程序(如WAR包)安装并运行在一个独立安装的、位于项目外部的Tomcat服务器上,而不是使用内嵌的或开发环境自带的服务器。 外部Tomcat 指独立安装的Tomcat服务器(如从Apache官网下载的Tomcat&#…...
小迪安全109-php模型动态调试,反序列化,变量覆盖,tp框架,原生pop链
案例一 原生代码变量覆盖,duomicms 变量覆盖是什么 这个时候为什么会输出xiaodi呢 就是因为$ab所以$$a$b 所以就让$bxiaodi 搞不到源码,看老师的学吧 这一般在web端页面是看不到,也不好发现,代码可以看关键字和函数 例如$$这…...
Python 练习项目:MBTI 命令行测试工具
在当今数字化的时代,心理测试工具越来越受到欢迎,它们帮助人们更好地了解自己,做出更明智的职业选择,甚至改善人际关系。MBTI(迈尔斯-布里格斯性格分类法)是其中一种广为人知的人格测试,通过评估个人在四个维度上的偏好(外向-内向、实感-直觉、理智-情感、判断-理解),…...
zynq7000 + ucos3 + lwip202_v1_2调试过程
1 现在裸机应用上验证lwip 跑起来可能会报错,看下面的链接解决 zynq 网卡Phy setup error问题 zynq 网卡Phy setup error问题-CSDN博客 2 ping同以后,在zynq上添加ucos系统 链接如下: ZYNQ移植uCOSIII_zynq ucos-CSDN博客 3 移植lwip协议…...
Oracle数据库数据编程SQL<2.2 DDL 视图、序列>
目录 一、Oracle 视图(Views) (一) Oracle 视图特点 (二)Oracle 视图创建语法 关键参数: (三)Oracle 视图类型 1、普通视图 2、连接视图(可更新) 3、对象视图 4…...
关于bug总结记录
1、vs中出现bug error C1083:无法打开文件 链接:vs中出现bug error C1083:无法打开文件_vs20151083错误解决方法-CSDN博客 2、 VS小技巧:系统却提示:示msvcp120.dll丢失 链接:VS小技巧:系统却提示:示msvc…...
ClickHouse与TiDB实操对比:从入门到实战的深度剖析
ClickHouse与TiDB实操对比:从入门到实战的深度剖析 宝子们,在当今数据驱动的时代,选择合适的数据库对于处理海量数据和支撑业务发展至关重要。ClickHouse和TiDB作为两款备受关注的数据库,各自有着独特的优势和适用场景。今天&…...
【测试开发】OKR 小程序端黑盒测试报告
【测试报告】OKR 小程序端 项目名称版本号测试负责人测试完成日期联系方式OKR 小程序端4.0马铭胜2025-03-2515362558972 1、项目背景 1.1 OKR 用户端 在如今这个快节奏的时代中,个人和组织的成长往往依赖于清晰、明确且意义深远的目标。然而,如何设定…...
LibVLC —— 《基于Qt的LibVLC专业开发技术》视频教程
🔔 LibVLC/VLC 相关技术、疑难杂症文章合集(掌握后可自封大侠 ⓿_⓿)(记得收藏,持续更新中…) 《基于Qt的LibVLC专业开发技术》课程视频,(CSDN课程主页、51CTO课程主页) 适合具有一些C++/Qt编程基础,想要进一步提高或涉足音视频行业的。本课程分7章节,共计35小节。…...
故障识别 | 基于改进螂优化算法(MSADBO)优化变分模态提取(VME)结合稀疏最大谐波噪声比解卷积(SMHD)进行故障诊断识别,matlab代码
基于改进螂优化算法(MSADBO)优化变分模态提取(VME)结合稀疏最大谐波噪声比解卷积(SMHD)进行故障诊断识别 一、引言 1.1 机械故障诊断的背景和意义 在工业生产的宏大画卷中,机械设备的稳定运行…...
2025年最新自动化/控制保研夏令营预推免面试真题分享(东南大学苏州校区/华东理工/南航/天大)
笔者来2021级本科自动化专业,以下部分将介绍我在夏令营以及预推免期间发生经历和问题 东南大学苏州校区蒙纳士大学联培 东南大学苏州校区的项目算是一个比较小众的项目,是第一年在苏州校区,二三年到南京校区找导师(不提供住宿自…...
深度学习笔记19-YOLOv5-C3模块实现(Pytorch)
🍨 本文为🔗365天深度学习训练营中的学习记录博客 🍖 原作者:K同学啊 一、前期工作 1.导入数据并读取 import torch import torch.nn as nn import torchvision.transforms as transforms import torchvision from torchvisio…...
SpringCloud-环境和工程搭建
前言 JDK用17,MySQL用8.0 微服务就是微小的服务 一个微服务只做一个事情 基本概念 打开官网 spring springcloud就是给我们提供工具,方便我们来弄微服务 springcloud是分布式微服务架构的一站式解决方案 Distributed/versioned configuration 分布…...
React 中shouldComponentUpdate生命周期方法的作用,如何利用它优化组件性能?
大白话React 中shouldComponentUpdate生命周期方法的作用,如何利用它优化组件性能? 在 React 里,shouldComponentUpdate 这个生命周期方法就像是一个“关卡守卫”,它能决定组件是否需要重新渲染。组件重新渲染是个挺费性能的事儿…...
AI写一个视频转图片帧工具(python)
现在的AI写python太方便了 说的话 我想用python实现一个能够将视频的所有帧数转化为图片的软件,可以自由配置转换的帧率或者全部,需要有界面,我需要增加一点功能,就是我需要读取出视频的分辨率,然后设置输出帧的分辨…...
React第三十一章(组件实战)
组件实战 这一章建议大家看完hooks css 原理 组件 这些章节之后再来看,这样会更好理解。 本章是额外新增的,因为之前的知识大家都掌握的差不多了,所以这一章节主要是让大家动手实践,巩固一下前面的知识。 那么你会学到什么&#…...
详细介绍WideCharToMultiByte()
书籍:《Visual C 2017从入门到精通》的2.7 字符串 环境:visual studio 2022 内容:几个字符串类型->(将单字节char*转换为宽字节wchar_t *)(将宽字节wchar_t* 转换为单字节char *) WideChar…...
Qt开发:QFileDialog的使用
文章目录 一、QFileDialog的介绍二、QFileDialog的常用静态方法三、完整示例 一、QFileDialog的介绍 QFileDialog 是 Qt 框架中提供的一个用于文件选择的标准对话框类,它允许用户在应用程序中选择文件或目录。它是跨平台的,在不同操作系统上会自动适配本…...
深度学习篇---回归分类任务的损失函数
文章目录 前言一、分类任务常用损失函数1. 交叉熵损失(Cross-Entropy Loss)数学形式使用场景特点训练状态分析损失下降损失震荡训练损失低但是验证损失高 2. Hinge Loss(合页损失)数学形式适用场景特点训练状态分析损失趋近于0损失…...
【MLP-BEV(10)】BEVPooling V1和BEVPooling V2的view_transformer,进行鱼眼图片实践
文章目录 先说说 BEVPoolv1步骤1:3D点生成步骤2 2D特征采样和BEV特征生成特点再谈谈BEVPoolv2步骤1:3D点生成步骤2: 计算索引关系步骤3: `voxel_pooling`计算鱼眼图片进行实践步骤1、3D点生成(基于Kannala-Brandt 进行调整)步骤2、2D特征采样和BEV特征生成(1) 体素化 (Voxe…...
上海SMT贴片技术解析与行业趋势
内容概要 随着长三角地区电子制造产业集群的快速发展,上海作为核心城市正引领着SMT贴片技术的革新浪潮。本文聚焦表面组装技术在高密度互连、微间距贴装等领域的突破性进展,通过解析焊膏印刷精度控制、元件定位算法优化等核心工艺,展现上海企…...
RAG优化:python从零实现[吃一堑长一智]循环反馈Feedback
本文将介绍一种有反馈循环机制的RAG系统,让当AI学会"吃一堑长一智",给传统RAG装了个"后悔"系统,让AI能记住哪些回答被用户点赞/拍砖,从此告别金鱼记忆: 每次回答都像在玩roguelike:失败结局会强化下次冒险悄悄把优质问答变成新知识卡牌,实现"以…...
Ubuntu 防火墙配置
Ubuntu 防火墙配置 一、UFW 基础操作二、核心规则配置三、高级功能四、特殊场景配置五、规则管理六、默认策略与安全建议七、故障排除 一、UFW 基础操作 检查 UFW 状态 sudo ufw status # 查看防火墙状态 sudo ufw status verbose # 显示详细规则 sudo ufw status numbered #…...
网络运维学习笔记(DeepSeek优化版) 024 HCIP-Datacom OSPF域内路由计算
文章目录 OSPF域内路由计算:单区域的路由计算一、OSPF单区域路由计算原理二、1类LSA详解2.1 1类LSA的作用与结构2.2 1类LSA的四种链路类型 三、OSPF路由表生成验证3.1 查看LSDB3.2 查看OSPF路由表3.3 查看全局路由表 四、2类LSA详解4.1 2类LSA的作用与生成条件4.2 2…...
【实战ES】实战 Elasticsearch:快速上手与深度实践-2.2.1 Bulk API的正确使用与错误处理
👉 点击关注不迷路 👉 点击关注不迷路 👉 点击关注不迷路 文章大纲 Elasticsearch Bulk API 深度实践:性能调优与容错设计1. Bulk API 核心机制解析1.1 批量写入原理剖析1.1.1 各阶段性能瓶颈 2. 高性能批量写入实践2.1 客户端最佳…...
Oracle 23ai Vector Search 系列之2 ONNX(Open Neural Network Exchange)
文章目录 Oracle 23ai Vector Search 系列之2 ONNX(Open Neural Network Exchange)ONNX基本概念ONNX(Open Neural Network Exchange)ONNX Runtime ONNX Runtime 在Oracle数据库中的集成参考 Windows 环境图形化安装 Oracle 23ai Oracle 23ai Vector Search 系列之1 架构基础 Or…...
前端 - ts - - declare声明类型
在使用typeScript的项目中 需要声明属性类型 单独的局部属性 可以直接在当前文件中声明 全局属性需要在项目根目录下新建.d.ts文件 vite会自动识别.d.ts类型文件 在该文件中使用declare声明类型有三种写法 1、在某种类型的文件中声明 2、声明window上的属性类型 3、全局声明…...