欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

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

程序员文章站 2022-06-12 10:29:29
...

 

一、环境变量

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

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

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

方法一:

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

例如:

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

 

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

 

方法二:

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

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

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

HISTSIZE:保存历史命令的条数

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

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

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

 

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

来看下面的例子:

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

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

 

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

 

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

 

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

 

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

 

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

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

 

 

我们分别用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;
}

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

进程的创建,fork()和vfork()的不同以及认识虚拟地址空间,环境变量的了解
那再看一下代码执行后,结果是怎样的:

 

#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;
}

 

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

 

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

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

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

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

  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;
}

执行结果:

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

 

 

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

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

 

 

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

用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;
}

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

 

 

 

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

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

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

分析:

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