进程的创建,fork()和vfork()的不同以及认识虚拟地址空间,环境变量的了解
一、环境变量
我们说程序是一个可执行的二进制代码,在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;
}
我们看到这里的全局变量的地址相同但是值却不同了,如果这里是我们真实的物理内存,很明显这种情况是不可能发生的,那么这里的地址到底是什么呢,
当一个进程在执行时,操作系统为其分配了一个与物理内存大小相同的空间,称为虚拟地址空间,在进行取数据时,虚拟地址与实际的物理地址之间建立一种映射关系,实现如下图:
采用虚拟地址空间的好处:
- 保证了进程之间的独立性(每一个进程不会随意访问其他进程的数据)
- 提高了执行效率,VA到PA的映射会给分配和释放内存带来方便(不连续的物理空间可以映射为一段连续的虚拟地址空间)
- 保证读写数据的安全性(物理内存本身是不限制访问的,就会被随意修改)
创建进程的另外一种方法:
我们知道 创建子进程用到系统调用fork()
还有一种创建子进程的方法是vfork();
- vfork()用于创建一个进程,而子进程和父进程共享地址空间(fork()的子进程具有独立的地址空间)
- 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()也是不一样
- main()函数中调用return(),是将整个进程结束掉
- 而在普通函数中调用return(),只是结束该函数,返回该函数调用处,程序从其调用处的下一行开始执行
- _exit()在任意位置使用时,都会使程序结束
分析:
我们上面的代码中子进程结束时没有调用_exit(),那么它执行完后,会走到main()函数的return 0处,return 0,是一种正常的退出,返回其调用前,继续执行,所以会出现循环执行的结果,并且知道当一个函数走到return 处时,该函数的栈帧就销毁了,但你还再次想执行函数时,就会发生段错误。
上一篇: 关于golang的迷宫广度优先算法
下一篇: NOIP2002过河卒