一、命令行参数
我们在编码的时候一直都有使用main这个函数,但是我们在用的时候main括号里面都是没有参数的,那main括号里面就一定不能有参数吗?
//带参数形式
int main(int argc, char *argv[])
{
// 程序代码
return 0;
}
mian括号中的两个参数分别是什么呢?是什么意思呢?
argc表示命令行参数的个数,包括程序名称本身
argv表示存储命令行参数的字符串数组。argv[0]通常存储程序的名称(如./a.out),argv[1]存储第一个参数(如hello),argv[2]存储第二个参数(如world),依此类推
由上图我们可以发现argc里面存的居然是命令行?
上述我们发现运行程序加上我们自己写的选项就可以实现对应选项的功能,就像我们在linux中输入ls -l这样的选项的时候可以实现该指令对应选项的功能,那选项是什么呢?
选项本质是字符串,它可以以一定的方式传递给ls内部的main函数,在ls内部实现的时候就可以根据不同的选项实现类似功能不同的表现形式。
在刚刚启动程序的时候命令行上输入的参数被父进程bash拿到,因为在命令行上启动的进程都是bash的子进程
命令行参数会被父进程维护成一个char* argv指针数组,父进程会把这个指针数组传给子进程,这个指针数组的最后一个元素必须以NULL结尾。
总结:命令行参数至少要有一个;进程对应的程序的名字一般是argv[0];有几个子串argv就是几,
二、环境变量
什么是环境变量?
基本概念:
环境变量(environment variables)⼀般是指在操作系统中用来指定操作系统运行环境的⼀些参数
环境变量通常具有某些特殊用途,还有在系统当中通常具有全局特性
常见的环境变量
PATH : 指定命令的搜索路径
HOME : 指定用户的主工作目录(即用户登陆到Linux系统中时,默认的目录)
SHELL : 当前Shell,它的值通常是/bin/bash。
现在我们来看一下这个图片
我们会看到为什么./hello就可以直接使用做出指定的操作,而我们输入hello却报出如下的错误呢?主要原因是没有这个命令,也就是说系统是给我们进行查找了这个命令发现没有找到后就报错了,但是在哪里找呢?类似于我们知道ls这个命令是在usr\bin\路径底下的,这个路径是被包含在PATH环境变量中的,所以系统就可以找到它,综上所述,操作系统查找可执行命令是在环境变量PATH中查找的
。
下面我们用两种方法来证明操作系统是在环境变量种查找的。
查找环境变量的方法:echo $NAME //NAME:你的环境变量名称
当我们清空环境变量的时候系统命令将无法执行
最后我们再将上面我们删除的环境变量的路径复制粘贴回去我们发现系统命令又可以执行了
下一种证明方法则是将当前命令的路径添加到环境变量中具体做法有两种:
第一种:使用export命令(export PATH=$PATH: + 路径)
第二种:PATH=$PATH: + 路径
系统在查找命令的时候会以冒号作为分隔符
env 显示所有的环境变量
history 查找历史命令数量
下面我们来介绍一下如何多方法获取环境变量
首先根据我们开头说的main()函数的参数,除了上面两个参数之外,其实还有一个参数char* env[]指针数组,它是指向一个个字符串,就是环境变量K=V的字符串
利用char* getenv(const char* name)
获取单个环境变量
这里我们就会想是进程获取了环境变量,实则不然是你的父进程bash获得了环境变量形成环境变量表的。
那我们又该怎么样获取父进程bash的环境变量表呢?子进程是可以继承父进程bash的代码和数据的。
父进程会从系统的配置文件中得到环境变量表,父进程会形成两张表,分别是:命令行参数表,环境变量表,前者一直在变化,后者比较稳定,它们都是内存级的。
三、程序地址空间
看上边这个C\C++的内存空间布局图,我们再来运行以下代码
代码运行结果:
由上面的运行结果我们可以看到正文代码到内核空间的地址是由低地址到高地址的。
我们可以看到静态变量的地址和全局变量的地址很接近,说明静态变量已经和全局变量的生命周期是一样的
虚拟地址
下面我们来观察以下这两个代码运行的结果
代码1
代码运行结果:
代码2
代码运行结果:
解析:第一段代码我们可以看到父子进程访问的是同一个全局变量,这个就能说明父子进程的代码和数据是共享的
。第二段代码我们发现子进程里面的g_val一直不断的增加,我们认为父子进程访问的是同一个变量因为地址是一样的,但是为什么得到的数据却是不一样的呢?因为进程是具有独立性的
但是即使是这样但是一个地址怎么可能查出不一样的值呢?这里我们就要理解一个叫虚拟地址
的东西了。
编译链接后的可执行程序存储在磁盘中,待运行时将被加载到内存。操作系统会为该进程创建进程控块(PCB),其中包含硬件上下文数据、进程ID等信息。需要注意的是,PCB并非直接指向程序代码和数据,而是通过虚拟内存机制实现关联。在创建PCB的同时,操作系统会建立虚拟地址空间和页表映射,该虚拟地址空间的地址范围通常是从全0到全F。
我们以32位机器为例子,虚拟地址空间的范围是[0,2^32 - 1],将来虚拟地址要和物理地址进行一一映射,就需要通过一种数据结构页表来实现
进程在进行访问内存的时候要先经过虚拟地址到物理地址的映射,找到物理内存才能访问数据。
如果我们创建一个子进程能否通过页表查询对应的物理内存呢?答案是肯定的,因为子进程也会有自己的PCB,虚拟地址空间,页表,子进程的这些都是以父进程为模板的,也就是浅拷贝,所以上述我们初始一个全局变量的g_val,子进程g_val和父进程是一样的,这就是刚才我们父子进程访问同一个变量数据和地址相同的原因。
当子进程修改数据时,操作系统会在物理内存上开辟新的空间,拷贝原来的数据,再进行写入,并更改映射关系。我们把这个技术叫做写时拷贝
所以上面所有的工作都没有改变虚拟地址,但是底层虚拟地址通过页表映射,子进程已经有了新的物理地址了,这就是刚才我们父子进程访问数据不一样但是地址一样的原因了。
直接使用物理内存会导致严重的安全隐患。当进程出现野指针等错误时,可能会访问到其他进程的内存数据。引入虚拟地址空间后,相当于在系统中增加了一个软件隔离层。所有内存访问都必须经过虚拟地址到物理地址的转换过程,这个过程中可以进行安全检查,从而有效防止跨进程的内存访问,确保每个进程的内存空间相互隔离,维护了系统的安全性。
可执行程序是在磁盘上的,它加载到物理内存的时候就变成了进程。虚拟地址空间是按照一定的区域划分好的,在一段范围内它一定都是代码或者已初始化的数据又或者未初始化的数据,在整体上,它一定是按照虚拟地址空间的布局进行划分区域的,在物理内存上是随机的。
在创建进程时,内核会首先建立相关数据结构。然而,是否必须立即加载进程的代码和数据呢?实际上,我们可以先完成进程控制块(PCB)的创建,并将其放入调度队列,待进程被调度时再加载代码和数据,同时建立内存映射关系。这种边执行边加载的技术被称为惰性加载
所以说新建的进程并不是立即调度的,可以先不加载代码和数据,等到需要的时候,别的进程就不用了,在加载你的代码和数据,这样可以提高内存的使用率