一、进程所需的运行环境有哪些?
所需环境有:启动代码、环境变量、c程序的内存空间布局、库等。 (1)启动代码 (a)什么是启动代码? 故名思意就是启动程序的代码,其实所有高级语言编写的程序,都有启动代码,对于我们 C语言程序来说也是如此,也有自己的启动代码。 (b)c程序的启动代码 大家都知道,c程序都是从main函数开始运行的,不过我们平时只看到main调用别的子 函数,但是main本身作为一个函数,其实main也是需要被人调用的,被谁调用的呢? 答:被C程序的启动代码调用的。 既然启动代码如此重要,那我们就需要认真的说说启动代码,在讲启动代码时,我们会 把以下问题介绍清楚: · 基于裸机运行的c程序 与 基于OS运行的c程序,他们在运行是有什么区别。 · 命令行的参数是如何传递给main的形参的。 · 什么是进程的正常终止 和 异常终止,return、exit、_exit这几种返回方式有什么异同 说到return、exit,其实在c++/java等中也有,它们的功能其实都是一样的。 · main函数调用return关键字时,到底发生了什么,返回的返回值到底返回给了谁, 有什么意义。 (2)环境变量表 进程在运行的过程中需要用到环境变量,而环境变量存放在了环境表中,所以我们需要讲一 讲环境变量表。 如果大家在学习java时安装过JDK的话,估计对windows的环境变量不陌生,因为安装JDK 时需要设置windows的环境变量,当然本章重点讲的是Linux的环境变量,不过为了便于理解, 会与windows的环境变量进行对比介绍。 (3)c程序的内存空间结构 c程序运行时,是运行在内存上的,也就是说需要在内存上开辟出一块空间给c程序,然后将C 代码会被从硬盘拷贝到内存空间上运行,而且这段空间必须布局为c程序运行所需的空间结构,c 程序才能运行,所以,c程序的内存空间结构也是必须要的“进程环境”。 比如程序在调用函数时需要用到“栈”这个东西,那么就必须在内存空间中构建出“栈”,否者c 序程序没办法实现函数调用。 这就好比你租了写字楼的某层开了家公司,但是这个空间肯定是需要被布局为你要的结构的 ,如果空间不布局的话,你公司怎么运作起来。 所有高级语言的程序在运行时,都有自己的内存空间和空间结构,不过它们的结构都是相似的 ,理解了C的内存空间布局,自然也理解其它程序的内存空间结构。 有关c程序的内存空间结构,我们在《C语言深度解析》中有详细讲过,但是由于c内存空间也 是“进程环境”之一,因此我们这里还会进行回顾。 (4)库 c程序(进程)运行时,都是需要库的支持的,至于为什么需要库,学过了c语言同学,应该都 非常清楚。 不仅c程序需要库的支持,其它高级语言的程序,同样需要库的支持,所以库也是非常重要的 进程环境,没有库的支持,我们的程序基本做不了太过复杂的事。 有关C库这一块,我们在《C语言深度解析》这门课里面有详细讲解,比如: · 为什么需要库 · 什么是静态库,什么是动态库 · 如何制作动态库和静态库 · 等 所以在这里,不再详细讲库这个东西,在这里大家只需要理解,库是也是非常重要的进程环境。
二、本章的意义
本章的API并不多,也不复杂,当然这些API都是与进程环境相关的API,对本章来说,理解这些API是其次, 更关键的还是希望大家理解什么是进程环境。 通过对本章“进程环境”的理解,希望能够有效解决大家以前很多的疑惑,比如: 1)main函数是被谁调用的 2)main函数的返回值返回给了谁 3)main函数的参数有什么用 4)什么是进程的环境变量表 5)什么是程序的内存空间,程序的内存空间为什么要进行结构的布局
三、启动代码
3.1 启动代码的作用
在上堂课就说过,顾名思意就是用来启动整个程序的代码,所有高级语言的程序,都有自己的启动代码。 C程序运行时,最开始运行的是启动代码,启动代码再去调用main函数,然后整个C程序都已运行。 ....... | | 子函数 | | 子函数 | | main函数 | | 启动代码 总之,高级语言程序 = 启动代码 + 自己代码。 所以,C的启动代码其实才是整个c程序的开始代码,不过由于启动代码并不是我们自己写的,所以很多 初学的同学并不知道还有启动代码这回事。 疑问:启动代码都做了些什么呢? 后面再介绍。
3.2 启动代码是由谁提供的
(1)启动代码一般都是由编译器提供的,一般有两种提供方式 1)源码形式 以源码形式提供时,编译器会将启动代码的源文件和自己程序的源文件一起编译。 编译 启动代码.c ————————————> ***.o \ \ \ 链接 ————> 可执行程序 / / 我的程序***.c ... ————> ***.o ***.o .../编译 比如我们后面学习单片机时,像开发单片机这种没有OS的计算机的C程序时,启动代码一般是源码 形式提供的,后面讲到单片机时,大家就能见到这种情况。 当然像单片机这种没有OS的裸机程序的启动代码都很简单。 2)二级制的.o(目标文件)形式 直接以.o形式提供时,省去了我自己对“启动代码”的编译。 启动代码 ***.o ***.o \ \ \ 链接 ————> 可执行程序 / 编译 / 我的程序 ***.c ***.c ... ——————————> ***.o ***.o .../ 一般来说,如果开发的程序是运行在OS上时,那么编译器一般是以.o形式来提供启动代码,比如 我们gcc a.c时,gcc就是以.o形式提供的,基于OS运行的程序的启动代码,相对而言,自然比较复 杂些。 gcc时加一个-v选项,查看gcc编译链接的详细情况时,可以看到有很多.o,这些.o就是gcc提供 的启动代码。 (2)gcc -v 在一般情况下,不管是在命令行使用gcc命令(编译程序)方式编译还是使用IDE的图形界面方式 编译,编译链接的中间过程都被省略了,当然图形化的编译方式,最终调用的还是gcc这种编译程序 来实现的,只不过一个图形化调用的,另一个是在命令行输入命令来调用的,但是本质是一样的。 由于编译的过程被屏蔽了,因此大家对启动代码基本没有什么感觉。 演示: gcc -v a.c /usr/lib/gcc/x86_64-linux-gnu/5/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/ 5/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/5/lto-wrapper -plugin-opt=-fresoluti on=/tmp/ccgWwfyD.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-o pt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --sysroo t=/ --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x 86-64.so.2 -z relro /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crt1.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/5/crtbegin.o -L/usr/lib/gcc/x86_64-linux-gnu/5 -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/5/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/5/../../.. /tmp/ccZBIWKc.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/x86_64-linux-gnu/5/crtend.o /usr/lib/gcc/x86_64-linux-gnu/5/../../../x86_64-linux-gnu/crtn.o 在编译的详细信息里面,有很多的事先就被编译好的.o文件,这些.o文件就是用来生成启动代码的。 gcc -v显示的详细信息,在讲《c语言深度解析》时,有过较为详细的介绍,不清楚的同学请看 《c语言深度解析》。
3.3 启动代码做了些什么
启动代码使用什么语言编写的 基本都是汇编写的。 启动代码大致做了些什么呢? 大致上有两件重要的事情: · 对c程序的内存空间进行布局,得到c程序运行所需要的内存空间结构。 · 留下相应库接口 (1)对c内存空间进行布局 我们前面说过,c等高级语言程序在运行时,函数调用需要“栈”,启动代码就需要在c内存空间上建 立“栈”,说白了就是从从c内存空间中划出一段空间,然后以“栈”的形式来进行管理。 · 疑问:栈是怎么建立起来的? 后面讲uboot移植时,uboot的最开始的汇编代码就是启动代码,其中有一段汇编代码就是用 来建立栈空间的,讲到uboot时在来详细介绍。 当然uboot比较特殊,平时进行c程序开发中,我们都不需要关心启动代码,因为都由编译器自动 提供了,所以对于启动代码这个东西,我们不用深究,而且深究明白了也就那么点东西,对实际开 发来说意义不大。 因此关于启动代码,重点是理解什么是启动代码,它大概是一个什么样的存在。 · 思考:为什么启动代码,基本都是使用汇编来编写? 在程序的内存空间结构还没有布局起来之前,高级语言程序还无法运行,此时只能使用汇编, 当利用汇编编写的启动代码将高级语言的内存空间结构建立起来后,自然就可以运行c/c++等高 级语言的程序了。 (2)为库的调用预留接口 如果程序使用的是动态库的话,编译时,动态库代码并不会被直接编译到程序中,只会留下相应的 接口,程序运行起来后,才会去对接库代码,为了能够对接动态库,启动代码会留下动态库的对接接口。 (3)等等 如果是c++等面向对象的启动代码的话,还会做一些其它的事情,我们这里就不介绍了。
3.4 OS具体是怎么实现拷贝的呢?
不管使用哪一种方式来启动程序,启动后OS会调用exec函数(加载器),这个加载器去加载程序时,会 自动的将硬盘上的代码拷贝到内存上运行起来,加载的过程其实就是拷贝的过程。 只不过有OS时,特别是当OS有虚拟内存机制时,exec不会拷贝全部代码,只会拷贝当前运行所需要的 代码,当这段代码运行完毕后,再拷贝另一段代码到内存中。
四、进程(程序)的终止方式
4.1 正常终止
进程主动调用终止函数/返回关键字所实现的结束,就是正常终止。 · main调用return关键字结束 · 程序任何位置调用exit函数结束 · 程序任何位置调用_exit函数结束 1)main函数调用return关键字,实现正常终止 return关键字的作用是返回上一级函数,如果main函数的子函数调用return的话,返回的上一 级是main函数。如果main函数调用return的话,main函数所返回的上一级是启动代码。 main函数调用return有两种方式: (a)显式调用 比如:return 返回值; 这个返回值也被称为进程的终止状态。 · 返回值的意义 返回值标记的了进程的终止状态,比如一般情况下。 return 0:正常结束 return -1:代表了某种操作失败 return -2:代表了另一种的操作失败 当然什么值代表了什么操作失败,其实可以由程序员来自己规定。 疑问:如果return时我不写返回值会怎样呢? 答:编译能过,但是有警告,因为与main函数的返回值要求不相符合。 明确的写出return,但是又不写明返回值的话,返回值不定,所谓不定的意思是, main函数最后一次调用的子函数所返回的返回值是多少,main函数就会使用这个返回 值来返回。 演示: 使用echo $?可以查看程序运行后的,main函数的返回值。 如果main函数没有调用任何子函数,而且return又没有返回任何值的话,默认返回-1。 疑问:返回值不定,是不是很危险? 不定这个词听着很危险,但其实并不危险,因为一般情况下根本用不到这个返回值, 所以“不定”也不是什么大事。一般我们都会显式写出return,而且都会返回一个返回值, 主要是你不这么做,编译器报警高,所以还是按照严格的写法来写。 · 疑问:是谁来接收这个返回值的,这个返回值可以用做什么? 加到后面课程时再来解释。 (b)隐式调用: 就是不明写出return,当main函数中的最后一句代码执行完毕后,会默认的调用return返回, 不过隐式return时,默认返回0。 2)在程序的任意位置调用exit函数 注意,我这里说的是,在程序中任何位置调用exit都有效,也就说不管是在main函数中调用 ,还是在main的子函数中调用,甚至是在子函数的子函数中调用都是有效的。 其实,main函数调用return返回到启动代码后,启动代码也是调用exit函数来实现正常终止的。 (a)exit函数原型 #include <stdlib.h> void exit(int status); 看到stdlib.h这个头文件,我们就知道exit函数是一个c库函数。 这个参数就是返回值(进程终止状态)。 main函数调用return将返回值返回给启动代码后,启动代码又会调用exit(返回值),将返回值 返回。 (b)代码演示 3)在程序的任意位置调用_exit函数 _exit是一个系统函数(系统API),而exit是c库函数,exit就是调用_exit来实现的。 既然exit也是靠调用_exit来实现的,那么我们也就可以直接调用_exit了。 不过更多时候,我们调用的还是exit函数,因为exit对_exit封装后,exit额外还做了好些事情 ,至于做了些什么事情,这个留到后面再来解释。 (a)_exit函数原型 #include <unistd.h> void _exit(int status); 用法与exit一样。 4)return、exit和_exit的返回值问题 · return main函数 启动代码 return(0) ——————> exit(0) ————————> _exit(0) ————> Linux OS · exit exit(0) ————————————> _exit(0) ————> Linux OS · _exit _exit(0) ————>Linux OS 有OS时,不管是采用哪种方式实现正常终止,返回值都会被返回给OS。 疑问:将返回值返回给OS有什么意义呢? 一般情况下没有意义,但是在有些特殊情况下是有意义的,只不过这种情况很少见,有关 这个问题我们留到进程控制时再来详细解释。
4.2 异常终止
进程不是因为return、exit和_exit函数而终止的,而是被强行发送了一个信号,这个信号将进程给无 条件终止了,这就是异常终止。 有关什么信号,在后面讲信号的章节,还会详细的介绍。 1)自杀:自己调用abort函数,自己给自己发一个SIGABRT信号将自己杀死,杀人凶器是信号; 2)他杀:由别人发一个信号,将其杀死,杀人凶器也是信号; 思考:为什么按下ctrl+c,就可以将程序终止。 演示: 其实向终端输入ctrl+c时,就是在向我的进程发送某个信号,然后这个信号将我的程序给异常终止了。
4.3 atexit函数
函数原型 #include <stdlib.h> int atexit(void (*function)(void)); 这是一个C库函数。 (1)功能:注册(登记)进程终止处理函数,参数就是被登记“进程终止函数”的地址。 当进程无论什么时候正常终止时,会自动的去调用登记的进程终止处理函数,实现进程终止时的 一些扫尾处理。 注意:我这里强调的是正常终止,不是异常终止。 (2)返回值 函数调用成功返回0,失败返回非零值,不会进行错误号设置。 (3)参数function 需要被注册的进程终止处理函数的地址。 (5)代码演示 1)从例子可以看出,进程终止处理函数的注册顺序和调用顺序刚好相反。 为什么顺序刚好相反呢? 这就是涉及到atxit函数的注册原理了。 调用atexit注册时,会将“进程终止处理函数”的函数地址压入进程栈中, 当进程正常终止时,又会自动从栈中取出函数地址,并执行这个函数,实现进程 的扫尾操作。 栈的特点是先进后出,先压栈的后调用,所以调用顺序刚好和注册顺序相反。 2)在Linux下,调用atexit最多可以允许登记32个终止处理函数。 3)同一个函数如果被登记多次,自然也会被调用多次。 4)在两种情况下,登记的进程终止处理函数不会被调用 (a)异常终止,不会调用 (b)直接调用_exit来正常终止时,不会调用 换句话说,只有使用return和exit来正常终止时,才会调用。 (6)登记“进程终止处理函数”有什么意义? 我们有的时候需要在进程正常终止时,做一些扫尾操作,比如将链表数据保存到文件中。 不过如果不使用进程终止处理函数的话,这个操作有点困难,因为进程有时候可能是因 为某个函数调用失败,然后在函数出错处理时调用exit(-1)终止的,但是你又无法预估哪一 个函数会出错,并在出错时调用相应的函数实现链表数据的保存,那怎么办呢? 这个时候就可以注册进程终止处理函数来实现了,因为进程终止时,会自动的调用终止 处理函数来实现进程的扫尾处理,比如将链表数据保存到文件中。
4.4 有OS时,进程从启动 到 正常终止的全过程
为什么调用exit、return正常终止时,会刷新标准io的缓存
有关标准IO的库缓存的缓冲有三种,无缓冲、行缓冲、全缓冲,有关这三个是怎么回事,我们在第三章 标准io中,已经讲得非常清楚了,不清楚的同学,请认真回看第三章。 (1)回顾行缓冲 标准输出(printf)的库缓存就是行缓冲的,在缓存中积压的数据,直到出现以下情况时, 才会刷新输出,否则就一直挤压着。 1)遇到\n时就刷新输出,\n表示这是一行,就好比句号表示一句话一样。 2)库缓存中数据满了,也会自动刷新输出,这就好比盆里的水满了溢出一样。 不过一般来说,数据不可能多到能够把缓存装满的。 3)调用标准fflush函数,主动刷新数据 4)调用fclose关闭标准输出时,会自动调用fflush刷洗数据 (2)为什么调用exit正常终止时,会刷新标准io的缓存呢? 因为exit会调用fclose关闭所有的标准io,关闭时会自动调用fflush来刷新数据。 这里要注意:如果进程时异常终止的话,是不会刷新缓存区的,因为异常退出时,跟exit函数半 毛钱关系都没有。
五、命令行参数
在命令行执行c程序时,可以输入命令行参数并传递给main函数的形参,然后进程就得到了命令行的参数。 其实在命令行执行c++、java程序时,也可以使用同样的方法,给进程传递命令行参数。 (1)命令行参数的用处 我们在第2章时,实现了一个my_ls程序,用于模拟ls命令,显示文件属性,执行my_ls时,就 需要跟接参数。 所有命令(程序:ls)的选项和参数,都是通过命令行参数得到的,所以对于命令行来说,命令行 参数是很有用的东西。 (2)代码演示 第一个参数永远都是程序名。 (3)将命令行参数传递给main函数形参的过程 构建指针数组 exec ./a.out *** *** ——————> 终端窗口进程 ——————> OS 内核 ——————> 启动代码 ——————> main函数形参
六、环境变量表
6.1 windows的环境变量
如果大家有因为学习java而安装过java所需的JDK(java开发工具包)的话,应该接触过环境变量这个 东西,因为安装JDK时就需要为JDK设置windows下的path环境变量。 为什么在命令行执行我自己的程序,需要指明路径 我们这里以windows为例,其实在Linux下也是这样的,不过Linux的情况留到后面再说。 在windows下,如果你不加路径的话,会道默认到当前路径下找程序,没有的话就找不到你的程序。 能不能不加路径,我随便在什么目录下都可执行我的程序呢? 当然可以,只要把程序所在路径,加入windows的path环境变量即可。 加入后,随便在什么位置,都可以不加路径的执行这个程序。 为什么设置了path环境变量后,可以不加路径就能执行程序 (1)path这个环境变量的作用 专门记录各种可执行程序所在路径。 (2)path记录后 执行程序时(命令时) 1)如果你有明确指定路径,那就直接到你指定的路径下找到程序并执行。 2)如果没有明确指定路径, 首先,在当前目录下找,如果不到这个程序,就会跑到path记录的各个路径下面 去找,如果有的话就执行这个程序,如果没有就提示没有个这程序。 所以,只要你把程序的所在路径加入path后,执行程序时就算不指定路径,系统 也会自动到path记录的各个路径下搜索。 (3)path的意义 如果说你想在命令行下任何位置,都能随心所欲的执行程序的话,这个path环境变量是很有 意义的。但是如果你是通过图形界面,双击快捷图标来打开程序的话,快捷图标可以自动找到 所指向的程序并执行,所以对于图形界面这种操作方式来说,path的意义不是很大。 一般来说,我们自己的安装的程序,都没有设置环境变量,所以如果想在命令行下执行的话, 要么到当前目录下执行,要么加路径执行。 (4)为什么执行mspaint时,也可以在任何位置不加路径执行 mspaint是windows自己提供的工具软件(画图),mspaint所在路径为c:\windows\ system32,这个路径被系统自动的加入了path环境变量,所以也能够在任何位置,不加 路径的执行程序。 再来看看windows的环境变量 (1)环境变量的组成 环境变量 = 环境变量名 + 环境变量数据 不管是环境变量名,还是环境变量数据,都是字符串。 大多数环境变量的数据,基本都是路径。 (2)之前操作的Path是一早就存在的环境变量,我们可不可以添加新的环境变量 当然可以,比如, 演示: 整个%环境变量名%,代表的就是该“环境变量”的数据,你可以认为%%就是用来获取环境 变量数据。 (3)环境变量被放在了哪里 被存放在了“环境变量表”里面。
6.2 环境变量表
(1)什么是环境变量表 用于存“放环境变量”的表,就是环境变量表。 什么是环境变量呢? 答:其实就是进程在运行时,会用到的一些字符串信息,环境表就好比是工具箱,里面放了各种 进程运行时需要用到的“工具”,比如各种的路径。 (2)环境变量文件 最原始的“环境变量表”都被保存在了“环境变量文件”中。 通过环境变量文件,实现了“环境变量”数据的永久保存。 我们通过图形界面设置、修改windows“环境变量”时,修改、设置的内容,都会被永久保存到“环境变 量文件”中。 可不可以直接修改“环境变量文件”,实现永久修改? 答:可以,不过在windows下,一般不会直接修改文件,而是通过图形界面来操作。 (3)每个进程的环境变量表 每一个进程都在自己的内存空间(堆空间)保存了一份自己的环境变量表。 如果你认真学习过我们《C语言深度解析》这门课的话,对于程序内存空间的“堆、栈”应该非常清楚。 · 每个进程空间中的环境变量表又是怎么来的? 显然从环境变量文件中得来的。 (4)如果某环境变量的数据有很多条,在环境变量表中,多条数据之间怎么区分 在windows这边使用;分隔,Linux这边则使用:分隔。 比如,windows的Path就有好多个数据(路径),这些数据在“环境变量表”中保存时,不同的数据之间 会使用;分隔。 Path=%SystemRoot%\system32;%SystemRoot%;%SystemRoot%\System32\Wbem;%SYSTEM ROOT%\System32\WindowsPowerShell\v1.0\ w10提供的图形操作界面将;省去了,w10之前的系统还能在图形操作界面看到;,但是实际在环境表中 保存时,多条数据之间是有;的。 (5)在命令行执行我的hellword程序时,查找的是谁的“环境变量表” 查找的是“命令行窗口进程”的“环境变量表”。 疑问:helloword进程有“环境变量表”吗? 答:当然有,因为我们说过,每个运行的进程都有自己的环境变量表,保存在自己的进程空间中。 (6)为什么只有重新打开“命令行窗口”后, 新设置的“环境变量”才生效? 因为新设置的环境变量,只是被保存到了windows的环境变量文件中,但是之前所打开的“ 命令行窗口”进程的“环境变量表”还没有得到更新,只有当重新打开后,才能更新。 其实在w10以前更麻烦,更新了环境变量文件后,必须要重启系统才能生效,不过w10后不用 这么麻烦了,只需要重新打开即可。
6.3 Linux的环境变量
Linux的环境变量与windows的环境变量在原理上其实是一样的。 修改Linux的环境变量表 (1)永久修改 1)图形方式操作 (a)windows 通过图形界面来修改"环境变量"是最常见的操作方式。 (b)ubuntu ubuntu跟windows一样,也提供了图形化的操作。不过在ubuntu这边,图形化修改 “环境变量”的操作并不常见,老实讲我也没有用过ubuntu的图形界面来修改过环境变量, 所以这里也不介绍了。 2)直接修改“环境变量文件” (a)windows 我们很少直接修改“环境变量文件”。 (b)ubuntu(Linux) 基本都是通过修改“环境变量文件”来实现的,不过有关如何修改Linux的环境变量文件 ,我们这里暂时不介绍,我们后面具体用到时,再来详细介绍。 (2)临时修改 · 什么是临时修改? 就是只修改当前进程自己的“环境变量表”,其它不相关进程的“环境变量表”以及“环境 变量文件”中数据,不会发生任何变化,当进程结束时,当前进程的“环境变量表”就被释放了 ,这就是临时修改。 · 如何实现临时修改 - 使用命令修改 使用命令修改的话,改的是“命令行窗口进程”的环境变量表。 - 通过API修改 在我们自己所写程序里面调用API,修改的只是我自己程序的“环境变量表”。 1)如何使用命令来修改“命令行窗口进程”的环境变量表 (a)查看所有环境变量 · Linux - 命令:export 把当前“终端窗口进程”的所有环境变量全部显示出来。 · windows - 命令:set 把当前“终端窗口进程”的所有环境变量全部显示出来。 (b)显示单个的环境变量 · Linux - 命令:echo - 用法:echo $环境变量命令 echo $PATH PATH是环境变量名,$PATH代表的就是PATH的环境变量数据,可以认为 $就是用来获取环境变量数据的。在Linux,环境变量的多个数据之间使用:分 隔,而在windows下,使用的是;来分隔。 · windows - 命令:set - 用法:set 环境变量名 或者 set %环境变量名% set Path 或者 set %Path% (c)添加一个新的环境变量 · Linux - 命令:export - 例子:export AA=wwwwwwwwwwww 新环境变量名叫AA,环境数据为wwwwwwwwwwww,不过这个环境 变量没有什么实际意义,仅仅就是为了演示如何来设置一个新的环境变 量。 演示: 在Linux下,环境变量名往往喜欢一律大写,而在windows下不 一定,有些大写有些小写。 而且一般来说,很少有需要去设置一个新的环境变量。 · windows - 命令set - 例子:set aa=wwwwwwwwwwww (d)修改已有环境变量 (1)覆盖式修改 覆盖原有数据,还是使用添加新环境变量的命令来操作 ,如果添加的“环境变量”之前就存在了,现在的数据会修 改原来的数据。 · Linux export AA=sssssssssss · windows set AA=sssssssssss (2)追加 保留原有数据。 · Linux - 例子1: export AA=$AA:dddddddddddd 或者 export AA=dddddddddddd:$AA - 例子2:将helloword程序的所在路径追加到PATH · windows - 例子1: set aa=%AA%;dddddddddddd 或者 set aa=dddddddddddd;%AA% - 例子2 将helloword.exe程序的所在路径追加到path (e)删除 · Linux unset AA · windows set aa= (f)看一看Linux的环境变量有哪些 演示: 其中,我们会经常用到的环境变量是PATH,与windows下的path的功 能是一样的,当我们不带路径的去执行某个程序时,就会到PATH指定的 路线下去找,找到后再执行,找不到就提示没有这个文件。 (g)注意 不管是在windows下还是在Linux下,在命令行下添加/修改环境变 量时,改动的只是当前“命令行窗口进程”的环境变量表。所以, · 当你在另一个窗口查看时,根本找不到这个修改。 · 当你把当前窗口关闭后,当前“命令行窗口进程”的环境变量表也就被释放了,那 么之前的修改自然也就无效了。 2)通过API修改环境变量 我们前面说过,对于我们自己所写的程序来说,我们可以调用API来修改自己所写 程序的“环境变量表”。 (a)获取环境表中的所有环境变量 · environ全局变量 char **environ; - environ与main函数的argv一样,指向的都是一个字符串指针数组。 只不过, argv:与命令行参数有关 environ:与环境变量表有关 · main函数的第三个参数 - 代码演示 (b)调用API:实现环境变量的添加、修改等 · putenv、setenv:添加和修改环境变量 - 函数原型 #include <stdlib.h> int putenv(char *string); int setenv(const char *name, const char *value, int overwrite); - 函数功能 + putenv:设置新的环境变量到环境表中。 如果这环境变量之前就存在,那么这一次的数据会无条件覆盖之前 的数据。如果不存在,就添加这个新的环境变量。 string:新的环境变量,比如“name=value”。 + setenv函数:功能同上。 name:环境变量的名字。 value:环境变量值。 overwrite:如果发现name环境变量以前就已经存在,会根据overwrite的 值来决定是否覆盖:0:不覆盖;!0:覆盖 + 函数返回值 putenv函数:调用成功返回0,失败返回非0,errno被设置。 setenv函数:调用成功返回0,失败返回-1,errno被设置。 - 代码演示 · unsetenv:删除环境变量函数 - 函数原型 #include <stdlib.h> int unsetenv(const char *name); + 功能 删除name指定名字的环境变量。 + 参数:char *name:环境变量名。 + 返回值:函数调用成功返回0,失败返回-1, errno被设置。 - 代码演示 (c)疑问:我自己所写程序的环境表是怎么来的 我命令行窗口执行./a.out,那么a.out进程就属于“命令行窗口进程”的子进程,子进 程的环境表是从父进程复制得到的,有关父子进程关系,后面的课程还会详细介绍。 当有OS支持时,基本所有的进程都是由父进程“生”出来的: 原始进程————>进程————————>进程————————>终端进程——————>a.out进程 | | | | | | V V | 进程 进程 进程 | | | | | | ... ... ... 所以所有进程的“环境变量表”都是从父进程复制得到的,最原始进程的“环境变量表”则 是从“环境变量文件”中读到的。 原始进程 子进程 子进程 环境变量文件 ————> 进程环境表 ————————>进程环境表 ------>...... 疑问1:最原始的进程从哪来的 答:OS启动完毕后演变得到的。 疑问2:如果我自己的进程有一个子进程的话,是不是也会继承我自己进程的环境表? 答:是的,不过这个问题我们留到后面章节再来详讲。 怎么验证这种继承关系? 修改了命令行终端的环境表后,在该终端下运行我自己进程,如果我的进程环境表也被改了, 自然就验证了这种继承关系。
七、c程序空间布局
1)什么是c程序的内存空间 程序运行的大致过程: c程序运行时,是运行在内存上的,也就是说需要在内存上开辟出一块空间给c程序,然后将 C代码会被从硬盘拷贝到内存空间上运行,至于说是不是将代码全部会被拷贝到内存上,这就 不一定的了, · 有些是全部考别,早期计算机和现在的单片机往往都是这种 · 有些是只拷贝当前要运行的那一部分,运行完了再拷贝其它部分 当运行有OS,而且OS提供了虚拟内存机制时,基本都是这种的情况,有关虚 拟内存机制的原理,请看《计算机体系结构》软件篇——操作系统的课程。 所以具体是那种情况,还要看具体的实现。 不过,我们这里重点是你要清楚,运行时需要要在内存开辟出一段空间,然后把代 码从硬盘拷贝到内存上,那么所开辟出的这段内存空间,就是c程序的内存空间。程序 的内存空间。 2)c程序的内存空间结构 (a)有结构的要求吗? 有,这段空间必须布局为c程序运行所需的空间结构,c程序才能运行。 比如程序在调用函数时需要用到“栈”这个东西,那么就必须在内存空间中构建出 “栈”,否者c序程序没办法实现函数调用。 如果空间没有布局好,进程将无法运行,因此进程空间(c程序的内存空间)也是 非常重要的进程环境。这就好比你租了写字楼的某层开了家公司,但是这个空间肯定 是需要被布局为你要的结构的,如果空间不布局的话,你公司怎么运作起来。 (b)c的内存结构是谁来构建的 由启动代码来搭建的,比如启动代码会把c内存空间的某一部分空间,构建为“栈” ,也就是说以“栈”的方式来管理这片内存。 (c)c程序的内存空间也其实也叫“进程空间” 为什么? 因为c程序在内存中运行起来后就是进程了,所以c程序的内存空间也其实也 叫“进程空间”。 c)不光是C程序 所有超级语言的程序在运行时,都涉及内存空间的结构布局,不过它们的结构都是 相似的,理解了C的内存空间布局,自然也理解其他程序的内存空间结构。 c程序代码在内存上运行起来后,它就是一个进程,所以程序代码在内存上所占用的空间,也别称为 进程空间。 c程序空间时c程序运行的非常总要环境,没有没有在内伤布局出这个空间环境,程序将无法运行起来。 原本打算还是简单介绍下的,但是由于这部分内容在C深度解析中,已经详细介绍过了,因此这里就不在介绍。
八、库
我写程序时,是绝对不可能从零开始写代码的,都是要依赖被人所写的代码的,比如别人写的库,所以 库也是程序非常重要的运行环境,没有库的支持,我们的程序根本做不了重要的事情。 有关C语言库这一块,我们也在C深度解析课程中做了详细讲解,所以这里也将其省略。