进程的创建,fork()和vfork()的不同以及认识虚拟地址空间,环境变量的了解

本文详细介绍了Linux系统中的环境变量概念及其设置方法,并通过实例解析了如何利用export命令导出环境变量。同时,文章深入探讨了进程创建的两种方式——fork()与vfork()的不同之处,并通过代码示例展示了这两种方法对全局变量的影响。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

 

一、环境变量

我们说程序是一个可执行的二进制代码,在Linux下一个命令也是一个程序,例如ls命令,我们知道当我们敲下ls,回车之后,系统就会执行这条命令。但是我们自己的可执行程序却要指明路径才可以执行。

ls命令是系统命令,是放在bin目录下。

如何让自己写的可执行程序也像系统命令那样不用指明路径来执行

方法一:

我们自己将自己写的命令(可执行程序)放到/bin 目录下,这样就是将自己写的命令置为系统命令

例如:

 

但是这样会污染我们的系统命令集,我不建议这样做,所以最后将我们添加的命令删除掉

 

方法二:

将自己的可执行程序路径放到环境变量中

认识一下常见的几个环境变量

HOME:指定用户的主工作目录

HISTSIZE:保存历史命令的条数

SHELL:当前Shell,一般是/bin/bash

PATH环境变量,指导操作系统搜索可执行成程序的路径

 

export :将本地变量导出为环境变量

来看下面的例子:

运行结果成功的打印出环境变量的值

 

 

当我们想打印自己在当前bash下定义的变量my_env时

 

 

我们试图用上面的函数印出环境变量的值,并且我们自己定义一个变量my_env,发生的段错误

 

 

 

我们分别用env和set命令来查看都有哪些变量,发现

用set命令查看的是所有的 环境变量(如HOME)和所有本地变量(本bash)(如my_env)

用env命令查看的是所有的 环境变量(如HOME)

 

环境变量具有全局特性,本地变量作用域只在本地,不会被子进程继承

其他查看环境变量的方法:

#include <stdio.h>                                                                                                                
  #include <stdlib.h>
  int main()
  {
       extern char **environ;
       int i=0;
       for(i=0;environ[i];++i)
       {
           printf("%s\n",environ[i]);
       }
        return 0;
 }

这里environ是指向环境变量表的指针,环境变量表是一个字符指针数组,每个指针为一个以'\0'结尾的环境变量字符串,数组最后一个元素为NULL;

 

下来看一下进程创建时的内存地址空间:

来看一个例子:

 

#include <stdio.h>                                                                                                                                                                          
#include <stdlib.h>
#include <unistd.h>
int g_val=100;//定义一个全局变量

int main()
{
   int  pid=fork();
    if(pid<0)
    {   
        perror("fork");
    }   
    else
    {   
        if(pid==0)
        {   
            printf("child,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
            exit(0);
        }   
        else
        {   
            sleep(3);//这里是保证了父进程在子进程后面调度
            printf("parent,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
        }   
    }   
    return 0;
}

结果为:全局变量的值和地址都相同,这也是我们预期的。


那再看一下代码执行后,结果是怎样的:

 

#include <stdio.h>                                                                                                                                                                                               
#include <stdlib.h>
#include <unistd.h>
int g_val=100;//定义一个全局变量

int main()
{
   int  pid=fork();
    if(pid<0)
    {
        perror("fork");
    }
    else
    {
        if(pid==0)
        {
            g_val=200;//对全局变量进行修改
            printf("child,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
            return 0;
        }
        else
        {
            printf("parent,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
        }
    }
    exit(0);
    return 0;
}

 

 

我们看到这里的全局变量的地址相同但是值却不同了,如果这里是我们真实的物理内存,很明显这种情况是不可能发生的,那么这里的地址到底是什么呢,

当一个进程在执行时,操作系统为其分配了一个与物理内存大小相同的空间,称为虚拟地址空间,在进行取数据时,虚拟地址与实际的物理地址之间建立一种映射关系,实现如下图:

采用虚拟地址空间的好处:

  1. 保证了进程之间的独立性(每一个进程不会随意访问其他进程的数据)
  2. 提高了执行效率,VA到PA的映射会给分配和释放内存带来方便(不连续的物理空间可以映射为一段连续的虚拟地址空间)
  3. 保证读写数据的安全性(物理内存本身是不限制访问的,就会被随意修改)

 

创建进程的另外一种方法:

我们知道 创建子进程用到系统调用fork()

还有一种创建子进程的方法是vfork();

  1. vfork()用于创建一个进程,而子进程和父进程共享地址空间(fork()的子进程具有独立的地址空间)
  2. vfork()保证了子进程先运行,在它调用exec或者exit之后父进程才可能被调度运行

其实就是在子进程运行期间父进程处于挂起(T)状态,子进程调度结束后,再来调度父进程。

来看下面代码:

#include <stdio.h>                                                                                                                                                                                               
#include <stdlib.h>
#include <unistd.h>
int g_val=100;//定义一个全局变量

int main()
{
   int  pid=vfork();
    if(pid<0)
    {
        perror("vfork");
    }
    else
    {
        if(pid==0)
        {
            printf("brfore:child,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
            g_val=200;//对全局变量进行修改
            printf("after:child,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
            _exit(0);
        }
        else
        {
            printf("parent,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
        }
    }
    return 0;
}

执行结果:

 

 

发现当子进程对全局变量进行修改之后,父进程中的值改变了,正如上面所说的子进程和父进程公用同一块地址空间

其实就是父进程和子进程公用同一张页表。地址空间模型如下图:

 

 

用vfork()创建子进程时,如果上面的代码将_exit(0)注掉:

#include <stdio.h>                                                                                                                                           
#include <stdlib.h>
#include <unistd.h>
int g_val=100;//定义一个全局变量

int main()
{
   int  pid=vfork();
    if(pid<0)
    {   
        perror("vfork");
    }   
    else
    {   
        if(pid==0)
        {   
            printf("brfore:child,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
            g_val=200;//对全局变量进行修改
            printf("after:child,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
        }   
        else
        {   
            printf("parent,pid:%d,ppid:%d,g_val:%d,&g_val:%p\n",getpid(),getppid(),g_val,&g_val);
        }   
    }   
    return 0;
}

 

 

 

看到子进程和父进程运行完成后,再次循环执行,知道看到发生了段错误。

我们知道在main()函数中调用return (),和普通函数调用return ()函数效果是不一样的,并且return ()和_exit()也是不一样

  1. main()函数中调用return(),是将整个进程结束掉
  2. 而在普通函数中调用return(),只是结束该函数,返回该函数调用处,程序从其调用处的下一行开始执行
  3. _exit()在任意位置使用时,都会使程序结束

分析:

我们上面的代码中子进程结束时没有调用_exit(),那么它执行完后,会走到main()函数的return 0处,return 0,是一种正常的退出,返回其调用前,继续执行,所以会出现循环执行的结果,并且知道当一个函数走到return 处时,该函数的栈帧就销毁了,但你还再次想执行函数时,就会发生段错误。

 

 

 

 

 

<think>好的,用户的问题是关于forkexec在创建进程时的区别及用法。我需要结合用户提供的引用信息以及之前回答中关于主动迁移负载均衡的内容来综合回答。首先,我需要明确forkexec各自的功能。 根据引用[2][3],fork用于创建进程,而exec用于替换当前进程的映像。用户可能想知道两者的具体区别,比如进程创建的方式、资源管理、使用场景等。同时,用户之前的问题涉及操作系统的进程调度,如主动迁移负载均衡,可能需要考虑这些机制如何与forkexec交互。 用户可能混淆了forkexec的作用,认为exec也会创建进程,但实际上exec只是替换当前进程的代码数据。需要明确这一点,避免误解。此外,用户可能想知道在实际编程中如何结合使用这两个系统调用,比如在shell中执行命令时的典型流程。 需要检查用户提供的引用内容,确保回答准确。引用[1]提到exec函数族替换当前进程,引用[2][3]详细说明了exec后的进程变化,而引用[4]进一步确认exec不创建进程。因此,回答中需要强调fork创建进程,而exec替换现有进程的内容。 另外,用户之前的回答中涉及进程迁移调度,可能需要在区别中提到进程创建与调度机制的关系,但主要焦点还是forkexec的区别。需要保持回答结构清晰,分点对比,并给出示例说明它们的用法,比如在shell中的使用。 可能还需要考虑错误处理,比如fork失败的情况,或者exec执行后的代码不可达。同时,提到vforkclone等其他相关系统调用作为扩展,但以用户的问题为主。 最后,生成的相关问题需要引导用户深入理解,比如资源继承、性能影响或典型应用场景。确保引用标记正确,比如exec替换进程的部分引用用户提供的引用[2][3][4]。</think>### fork与exec在创建进程时的区别及用法详解 --- #### **一、核心机制对比** | **维度** | **fork()** | **exec()** | |-------------------|----------------------------------------------------------------------------|----------------------------------------------------------------------------| | **进程创建** | 创建**新进程**(子进程),子进程是父进程的**完整副本** | **不创建进程**,仅替换当前进程的代码段、数据段、堆栈段等内存空间[^2][^3] | | **资源继承** | 继承父进程的**所有资源**(文件描述符、内存、信号处理等) | 继承原进程的**PID、PPID、文件描述符**(除非显式关闭)等元数据[^2][^4] | | **执行流程** | 子进程从`fork()`返回处开始执行,通过返回值区分父子进程 | 新程序从`main()`函数开始执行,原进程代码在`exec()`后**完全失效**[^3][^4] | | **典型应用场景** | 实现**多任务并行**(如Web服务器处理并发请求) | 实现**程序替换**(如shell执行外部命令) | --- #### **二、关键差异解析** 1. **内存空间变化** - **fork**: - 使用**写时复制(Copy-On-Write)**技术: $$ \text{物理内存消耗} = \text{父进程内存} \times (1 - \frac{\text{共享页比例}}{100}) $$ - 子进程获得父进程虚拟内存的**独立映射表** - **exec**: - 完全**覆盖**当前进程的代码段、初始化数据段、堆栈段 - 新程序的`.text`段加载地址通常与旧程序无关 ```c // fork示例 pid_t pid = fork(); if (pid == 0) { // 子进程代码 } else { // 父进程代码 } // exec示例(execlp) execlp("ls", "ls", "-l", NULL); // 执行后后续代码不再运行 ``` 2. **资源管理差异** - **文件描述符继承**: | **操作** | **fork** | **exec** | |---------------------|----------|----------| | 保持打开状态 | ✔️ | ✔️ | | 使用`FD_CLOEXEC`标志| - | ❌ | - 通过`fcntl(fd, F_SETFD, FD_CLOEXEC)`可在exec时自动关闭描述符[^2] 3. **性能开销** | **指标** | **fork()** | **exec()** | |----------------|--------------------------------|--------------------------------| | 内存拷贝开销 | 低(COW优化) | 高(需加载新程序到内存) | | 上下文切换频率 | 可能引发调度器介入 | 仅首次加载时产生显著延迟 | | 典型耗时 | 微秒级(依赖父进程内存大小) | 毫秒级(依赖磁盘I/O速度) | --- #### **三、协同工作模式** **经典组合:fork+exec**(shell命令执行标准流程) ```mermaid sequenceDiagram participant Shell participant Child_Process Shell->>Child_Process: fork()创建进程 Child_Process->>Child_Process: 关闭不需要的文件描述符 Child_Process->>Child_Process: 设置信号处理 Child_Process->>Child_Process: exec()加载新程序 Child_Process-->>Shell: 执行完成/出错退出 ``` 1. **执行细节**: - 子进程通过`fork()`获得shell的环境上下文 - 调用`execvp()`时会自动搜索`PATH`环境变量中的可执行文件[^1] - 若exec失败,子进程应调用`exit()`避免继续执行原代码 2. **错误处理示例**: ```c pid_t pid = fork(); if (pid == -1) { perror("fork failed"); exit(EXIT_FAILURE); } else if (pid == 0) { execlp("gcc", "gcc", "test.c", "-o", "test", NULL); // 若exec失败才会执行以下代码 perror("exec failed"); exit(EXIT_FAILURE); } else { wait(NULL); // 等待子进程结束 } ``` --- #### **四、高级特性对比** 1. **vfork()优化** - **设计目的**:避免fork的COW开销,在exec前**暂停父进程** - **内存行为**:子进程**共享父进程地址空间**直到调用exec或exit - **风险**:若子进程修改父进程数据将导致未定义行为 2. **clone()系统调用** - 允许更细粒度控制资源共享(如线程实现) - 通过参数选择共享的内容: ```c clone(child_func, stack_top, CLONE_VM | CLONE_FS, NULL); ``` --- #### **五、典型应用场景** | **场景** | **fork()使用** | **exec()使用** | |------------------------|---------------------------------------------|---------------------------------------------| | Shell命令执行 | 创建进程隔离环境 | 加载`ls`/`gcc`等外部程序 | | 守护进程 | 通过两次fork避免僵尸进程 | 无(通常不替换自身代码) | | 多进程服务器 | 为每个连接创建独立处理进程 | 无(各进程运行相同服务代码) | | 热更新系统 | 创建进程运行新版本 | 在新进程中加载更新后的二进制文件 | --- ### 关键区别总结表 | **特征** | **fork()** | **exec()** | |--------------------|-----------------------------|-----------------------------| | 进程ID | 生成新PID | 保持原PID | | 内存空间 | 初始为父进程副本 | 完全替换为新程序映像 | | 执行连续性 | 从fork返回处继续执行 | 从新程序的main函数开始执行 | | 典型返回值 | 子进程返回0,父进程返回子进程PID | 成功无返回,失败返回-1 | --- ### 相关问题 1. 为什么`fork()`后需要判断返回值?子进程如何避免执行父进程的代码? 2. 如何通过`exec()`系列函数传递环境变量? 3. `fork()`与`pthread_create()`在创建并发单元时有何本质区别? 4. 在多线程程序中调用`fork()`会产生哪些风险?如何规避? [^1]: exec函数族通过替换进程内存实现程序加载 [^2]: fork创建进程时继承父进程资源 [^3]: exec保持原进程PID但替换内存内容 [^4]: 新程序从main函数开始执行且覆盖原进程数据
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值