Table of Contents
2.4.2 为什么调用exit、return正常终止时,会刷新标准io的缓存
0.那么进程所需的运行环境有哪些?
所需环境有:启动代码、环境变量、c程序的内存空间布局、库等。
1. 启动代码
1.1 启动代码的作用
所有高级语言的程序,都有自己的启动代码。
C程序运行时,最开始运行的是启动代码,启动代码再去调用main函数,然后整个C程序都已运行。
总之,高级语言程序 = 启动代码 + 自己代码。
1.2 启动代码是由谁提供的
(1)启动代码一般都是由编译器提供的,一般有两种提供方式
1)源码形式
以源码形式提供时,编译器会将启动代码的源文件和自己程序的源文件一起编译。
像开发单片机这种没有OS的计算机的C程序时,启动代码一般是源码形式提供
2)二级制的.o(目标文件)形式
直接以.o形式提供时,省去了我自己对“启动代码”的编译。
如果开发的程序是运行在OS上时,那么编译器一般是以.o形式来提供启动代码
比如我们gcc a.c时,gcc就是以.o形式提供的,基于OS运行的程序的启动代码,相对而言,自然比较复杂些。
gcc时加一个-v选项,查看gcc编译链接的详细情况时,可以看到有很多.o,这些.o就是gcc提供的启动代码。
(2)gcc -v
gcc -v a.c
在编译的详细信息里面,有很多的事先就被编译好的.o文件,这些.o文件就是用来生成启动代码的
1.3 启动代码做了些什么
1.3.1 启动代码使用什么语言编写的
基本都是汇编写的。
1.3.2 启动代码大致做了些什么呢?
大致上有两件重要的事情:
· 对c程序的内存空间进行布局,得到c程序运行所需要的内存空间结构。
· 留下相应库接口
(1)对c内存空间进行布局
c等高级语言程序在运行时,函数调用需要“栈”,启动代码就需要在c内存空间上建立“栈”,
说白了就是从从c内存空间中划出一段空间,然后以“栈”的形式来进行管理。
· 思考:为什么启动代码,基本都是使用汇编来编写?
- 在程序的内存空间结构还没有布局起来之前,高级语言程序还无法运行,此时只能使用汇编
- 当利用汇编编写的启动代码将高级语言的内存空间结构建立起来后,自然就可以运行c/c++等高级语言的程序了。
(2)为库的调用预留接口
如果程序使用的是动态库的话,编译时,动态库代码并不会被直接编译到程序中,只会留下相应的接口
程序运行起来后,才会去对接库代码,为了能够对接动态库,启动代码会留下动态库的对接接口。
1.4 程序是如何运行起来的
1.4.1 裸机的情况
(1)内存和硬盘一体式
1)典型的比如51单片机,51没有单独的内存和单独硬盘,使用的是内存和硬盘功能二合一的norflash。
(a)为什么能身兼内存的功能?
因为norflash的访问速度很快,因此cpu能够直接从norflash上读取指令并执行,此时norflash就是一个内存。
(b)为什么身兼硬盘的功能?
因为norflash能够永久保存数据,设备关电后,数据依然存在
2)程序运行的过程
(2)内存和硬盘分开式
比如arm芯片
1)为什么传统的计算机,硬盘和内存都是分开的
2)内存和硬盘分开的这种情况,如果直接以裸机方式使用的话,程序是如何运行起来的
两种:
第一种:直接将下载到内存中,然后运行。
第二种:先下载硬盘永久保存,开机时自动从硬盘中将代码拷贝到内存上,然后运行。(a)将程序直接下载到内存
这种方式最大的缺点就是掉电就没了,所以这种方式只适合于平时的测试 。
(b)将程序下载到硬盘
1.4.1 有OS的情况
上OS的计算机,基本都是内存和硬盘分开式的情况。
OS是怎么运行启动起来的呢?
(1)有OS时,可执行程序都是直接放在了硬盘上
(2)有OS时,程序如何运行起来
1)有OS支持时,如何启动程序呢
(a)裸机时,是怎么启动程序的呢?
(b)有OS支持时,启动程序方式有三种
· 双击快捷图标运行
· 在命令行运行
· 设置为开机自启动
2)OS是怎么实现拷贝的对于我们自己写的裸机程序来说,我们需要自己写拷贝代码
对于OS这个裸机程序来说,由启动程序来负责代码的拷贝。
2. 进程(程序)的终止方式
2.1 正常终止
进程主动调用终止函数/返回关键字所实现的结束,就是正常终止。
· main调用return关键字结束
· 程序任何位置调用exit函数结束
· 程序任何位置调用_exit函数结束
1)main函数调用return关键字
return关键字的作用是返回上一级函数,如果main函数的子函数调用return的话,返回的上一级是main函数。
如果main函数调用return的话,main函数所返回的上一级是启动代码。
(a)显式调用
· 返回值的意义
疑问:如果return时我不写返回值会怎样呢?
(b)隐式调用
就是不明写出return,当main函数中的最后一句代码执行完毕后,会默认的调用return返回
不过隐式return时,默认返回0。
2)在程序的任意位置调用exit函数
其实,main函数调用return返回到启动代码后,启动代码也是调用exit函数来实现正常终止的。
#include <stdlib.h> void exit(int status);
这个参数就是返回值(进程终止状态)。
main函数调用return将返回值返回给启动代码后,启动代码又会调用exit(返回值),将返回值返回。
3)在程序的任意位置调用_exit函数
_exit是一个系统函数(系统API),而exit是c库函数,exit就是调用_exit来实现的。
4)return、exit和_exit的返回值问题
return、exit和_exit的返回值,也被称为进程终止状态。
(a)裸机时:
(b)有OS时:
return、exit、_exit,使用哪种来返回都行。
2.2、异常终止
进程不是因为return、exit和_exit函数而终止的,而是被强行发送了一个信号给无条件终止了,这就是异常终止。
2.3 atexit函数
函数原型
#include <stdlib.h> int atexit(void (*function)(void));
登记“进程终止处理函数”有什么意义?
2.4 有OS时,进程从启动 到 正常终止的全过程
2.4.1 图示
2.4.2 为什么调用exit、return正常终止时,会刷新标准io的缓存
有关标准IO的库缓存的缓冲有三种,无缓冲、行缓冲、全缓冲
(1)回顾行缓冲
标准输出(printf)的库缓存就是行缓冲的,在缓存中积压数据,直到以下情况时,才会刷新输出,否则一直挤压】
(2)为什么调用exit正常终止时,会刷新标准io的缓存呢?
- 因为exit会调用fclose关闭所有的标准io,关闭时会自动调用fflush来刷新数据。
- 这里要注意:如果进程时异常终止的话,是不会刷新缓存区的,因为异常退出时,跟exit函数半毛钱关系都没有。
2.5 命令行参数
第一个参数永远都是程序名
将命令行参数传递给main函数形参的过程
3. 环境变量表
3.1 windows的环境变量
3.1.1 为什么在命令行执行我自己的程序,需要指明路径
在windows下,如果你不加路径的话,会道默认到当前路径下找程序,没有的话就找不到你的程序。
3.1.2 能不能不加路径,我随便在什么目录下都可执行我的程序呢?
当然可以,只要把程序所在路径,加入windows的path环境变量即可。
3.1.3 为什么设置了path环境变量后,可以不加路径就能执行程序
(1)path这个环境变量的作用
专门记录各种可执行程序所在路径。
(2)path记录后
(3)path的意义
一般来说,我们自己的安装的程序,都没有设置环境变量
3.1.4 再来看看windows的环境变量
3.2 环境变量表
(1)什么是环境变量表
用于存“放环境变量”的表,就是环境变量表。
什么是环境变量呢?
答:其实就是进程在运行时,会用到的一些字符串信息,环境表就好比是工具箱,
里面放了各种进程运行时需要用到的“工具”,比如各种的路径。
(2)环境变量文件
(3)每个进程的环境变量表
每一个进程都在自己的内存空间(堆空间)保存了一份自己的环境变量表。
每个进程空间中的环境变量表又是怎么来的?
显然从环境变量文件中得来的。
(4)如果某环境变量的数据有很多条,在环境变量表中,多条数据之间怎么区分
在windows这边使用;分隔,Linux这边则使用:分隔。
(5)为什么只有重新打开“命令行窗口”后, 新设置的“环境变量”才生效?
3.3 Linux的环境变量
修改Linux的环境变量表
(1)永久修改
1)图形方式操作
2)直接修改“环境变量文件”
(2)临时修改
· 什么是临时修改?
就是只修改当前进程自己的“环境变量表”,其它不相关进程“环境变量表”及“环境变量文件”中数据,不会发生任何变化,
· 如何实现临时修改
1)如何使用命令来修改“命令行窗口进程”的环境变量表
(a)查看所有环境变量
(b)显示单个的环境变量
(c)添加一个新的环境变量
(d)修改已有环境变量
(d)删除
2)通过API修改环境变量
对于我们自己所写的程序来说,我们可以调用API来修改自己所写程序的“环境变量表”。
(a)获取环境表中的所有环境变量
1 environ全局变量: char **environ;
- environ与main函数的argv一样,指向的都是一个字符串指针数组。
argv:与命令行参数有关
environ:与环境变量表有关
2 main函数的第三个参数: char **environ
(b)调用API:实现环境变量的添加、修改等
· putenv、setenv:添加和修改环境变量
#include <stdlib.h> int putenv(char *string); int setenv(const char *name, const char *value, int overwrite);
· unsetenv:删除环境变量
#include <stdlib.h> int unsetenv(const char *name);
·getenv:获取环境变量
(c)疑问:我自己所写程序的环境表是怎么来的
我命令行窗口执行./a.out,那么a.out进程就属于“命令行窗口进程”的子进程,子进程的环境表是从父进程复制得到的
当有OS支持时,基本所有的进程都是由父进程“生”出来的:
疑问1:最原始的进程从哪来的
答:OS启动完毕后演变得到的。
4. c程序内存空间布局
1)什么是c程序的内存空间
c程序运行时,是运行在内存上的,也就是说需要在内存上开辟出一块空间给c程序,然后将C代码从硬盘拷贝到内存空间上运行,至于说是不是将代码全部会被拷贝到内存上,这就不一定的了
2)c程序的内存空间结构
(a)有结构的要求吗?
有,这段空间必须布局为c程序运行所需的空间结构,c程序才能运行
如果空间没有布局好,进程将无法运行,因此程序的内存空间布局是非常重要的进程环境。
(b)c的内存结构是谁来构建的
是由启动代码来搭建的,比如启动代码会把c内存空间的某一部分空间构建为“栈”,或者说以“栈”的方式来管理这片空间。
(c)不光是C程序
所有高级语言的程序在运行时,都涉及内存空间的结构布局,不过它们的结构都是相似的
5. 库
写程序时,绝对不可能从零开始写代码,都是要依赖别人所写的代码的,比如别人写的库,所以库也是程序非常重要的
进程环境,没有库的支持,我们的程序根本做不了什么复杂的事情。