程序
存放在硬盘上的、静态的机器指令文件-------静态就是没有运行的意思
进程
将硬盘上的程序代码拷贝到
内存中,正在被cpu执行的程序运行的程序:cpu从第一条指令开始,一条一条的执行程序中的所有指令代码
启动代码、环境变量、c程序的内存空间布局、库等
启动代码
就是启动程序的代码,其实所有高级语言编写的程序,都有启动代码c程序都是从main函数开始运行的,其实main也是需要被
C程序的启动代码调用的
环境变量
进程在运行的过程中需要用到环境变量,而环境变量存放在了环境表中,也就是
环境变量表。
c程序的内存空间结构
c程序运行时,是运行在
内存上的需要在
内存上开辟出一块空间给c程序,然后将C代码会被从硬盘拷贝到内存空间上运行这段空间必须布局为
c程序运行所需的空间结构,c程序才能运行所以,c程序的内存空间结构也是必须要的“进程环境”
比如程序在调用函数时需要用到“栈”这个东西,那么就必须在内存空间中构建出“栈”,也就是说以栈的形式去管理这块内存空间
内存空间是需要布局的,比如那些内存是代码区,哪些内存是数据区
库
库是也是非常重要的进程环境
c程序(进程)运行时,都是需要库的支持的
没有库的支持,程序基本做不了太过复杂的事
与进程环境相关的API
所有高级语言的程序,都有自己的启动代码
C程序运行时,最开始运行的是启动代码,启动代码再去调用main函数,然后整个C程序都已运行
.......
|
|
子函数2
|
|
子函数1
|
|
main函数
|
|
启动代码
高级语言程序 = 启动代码 + 自己代码
启动代码一般(99%)都是由编译器提供的
一般有两种提供方式
源码形式
以源码形式提供时,
编译器会将启动代码的源文件和自己程序的源文件一起编译
- 像开发
单片机这种没有OS的计算机的C程序时,启动代码一般是源码形式提供的编译 启动代码.c ——————————> ***.o \ \ \ 链接 ————> 可执行文件(可执行程序) / 编译 / 我的程序 ***.c ... ——————————> ***.o ... /
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
二级制的.o(目标文件)形式
直接以.o形式提供时,省去了我自己对“启动代码”的编译
启动代码 ***.o ***.o \ \ \ 链接 ————> 可执行文件(可执行程序) / 编译 / 我的程序 ***.c... ————> ***.o .../
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
一般来说,如果开发的程序是
运行在OS上时,那么编译器一般是以.o形式来提供启动代码
gcc就是以.o形式提供的,基于OS运行的程序的启动代码
gcc时加一个
-v选项,查看gcc编译链接的详细情况时,可以看到有很多.o,这些.o就是gcc提供的启动代码
gcc -v 文件名.c

在编译的详细信息里面,有很多的事先就被编译好的.o文件,这些.o文件就是用来生成启动代码的
启动代码基本由汇编语言编写
启动代码做了两件重要的事:
对c程序的内存空间进行布局,得到c程序运行所需要的内存空间结构
函数调用需要“栈”,启动代码就需要在c内存空间上建立“栈”
从c内存空间中划出一段空间,然后以“栈”的形式来进行管理
- uboot的最开始的汇编代码就是启动代码,其中
有一段汇编代码就是用来建立栈空间
留下相应库接口,方便与库进行链接
裸机只能调用静态库,但是有os时候,大部分用动态库
在程序的内存空间结构还没有布局起来之前,高级语言程序还无法运行,此时只能使用汇编,当利用汇编编写的启动代码将高级语言的内存空间结构建立起来后,自然就可以运行c/c++等高级语言的程序了。
比如C语言无法用C语言去写启动代码,比如C语言函数调用需要栈,但是启动代码需要布局栈结构。
如果程序使用的是
动态库的话,编译时,动态库代码并不会被直接编译到程序中,只会留下相应的接口,程序运行起来后,才会去对接库代码,为了能够对接动态库,启动代码会留下动态库的对接接口。
分为裸机和操作系统两种情况:
裸机的情况
裸机,就是没有OS
内存和硬盘一体式
51单片机-----没有单独的内存和单独硬盘,使用的是内存和硬盘功能二合一的norflash
norflash既是内存也是硬盘
- 因为norflash的
访问速度很快,因此cpu能够直接从norflash上读取指令并执行,此时norflash就是一个内存- 因为norflash能够
永久保存数据,设备关电后,数据依然存在,所以将程序下载到norflash后,关机重上电,程序依然还能运行
内存和硬盘分开式
为什么传统的计算机,硬盘和内存都是分开的?
像单片机这种内存和硬盘二合一的norflash形式,由于造价太高,只适合于单片机这种小容量的设备使用
1)传统的内存:cpu能够快速访问,但是掉电丢失数据
2)传统的硬盘:可以容量做的很大,掉电不丢失数据,但是访问速度很慢所以二者相互配合使用,同样能起到与norflash一样的二合一的效果,而且容量更高,单位造价更低
像内存和硬盘分开的这种情况,一般都要运行OS的,很少直接以裸机方式来来使用
内存中,然后运行
有OS的计算机,基本都是内存和硬盘分开式的情况
原因:计算机资源相对丰富,有能力跑OS,如果不上系统直接裸机使用的话,这其实是在浪费计算机资源
OS也是一个程序,而且是一个很大的裸机程序,因为操作系统直接运行在硬件上
重点:介绍基于OS运行的应用程序,是怎么在OS的支持下运行起来的
!!!!有操作系统时,启动程序:
※启动后OS调用加载器exec函数,这个加载器加载程序,然后会将硬盘上的代码自动拷贝到内存上,
加载的过程就是拷贝的过程,但是只会拷贝当前运行的代码而不是全部的代码,然后让PC指向第一条指令,cpu取指令执行,那么整个程序就运行起来了。
进程终止方式有两种,一种是正常终止,另一种是异常终止
进程主动调用终止函数/返回关键字所实现的结束,就是正常终止
main调用return关键字结束- 程序任何位置调用
exit函数结束- 程序任何位置调用
_exit函数结束
return关键字的作用是返回上一级函数
如果main函数的子函数调用return的话,返回的上一级是main函数
如果main函数调用return的话,main函数所返回的上一级是启动代码
main函数调用return有两种方式:
显式调用(return 返回值)
返回值的意义

如果main函数没有调用任何子函数,而且return又没有返回任何值的话,默认返回-1
在程序中任何位置调用exit都有效
不管是在main函数中调用,还是在main的子函数中调用,甚至是在子函数的子函数中调用都是有效的
其实,main函数调用return返回到启动代码后,启动代码也是调用exit函数来实现正常终止的
#include
//这个参数就是返回值(进程终止状态)
void exit(int status);

一般使用该函数进行报错处理
_exit是一个系统函数(系统API),而exit是c库函数
- exit就是调用_exit来实现的
- exit对_exit封装后,exit额外还做了好些事情
#include
void _exit(int status);
裸机时:
1: main函数 启动代码
return(0) ——————> exit(0) ————————> _exit(0) ————> Linux OS
2: exit
exit(0) ————————> _exit(0) ————————> Linux OS
3: _exit
_exit(0) ————————>Linux OS
----------有OS时,不管是采用哪种方式实现正常终止,返回值都会被返回给OS
进程不是因为return、exit和_exit函数而终止的,而是被强行发送了一个信号,这个信号将进程给无条件终止了,这就是异常终止
自杀—自己调用abort函数,自己给自己发一个SIGABRT信号将自己杀死,杀人凶器是信号
他杀—由别人发一个信号,将其杀死,杀人凶器也是信号
程序死循环—ctrl+c结束进程
其实向终端输入ctrl+c时,就是在向我的进程发送某个信号,然后这个信号将我的程序给异常终止
#include
void abort(void);

#include
//进程终止处理函数
//参数就是被登记“进程终止函数”的地址
/*
当进程无论什么时候正常终止时,会自动的去调用登记的进程终止处理函数,实现进程终止时的一些扫尾处理
------强调的是正常终止,不是异常终止
*/
//返回值
//函数调用成功返回0,失败返回非零值,不会进行错误号设置。
int atexit(void (*function)(void));
//function为void (*)(void)的函数指针类型

在第三手册查到了,说明是个库函数
#include
#include
void process_deal(void){
printf("deal process!!\n");
}
int main(){
atexit(process_deal);
printf("having used function()\n");
return 0;
}
/*执行结果:
having used function()
deal process!!
*/
#include
#include
void process_deal1(void){
printf("deal1 process!!\n");
}
void process_deal2(void){
printf("deal2 process!!\n");
}
int main(){
atexit(process_deal1);
atexit(process_deal2);
printf("having used function()\n");
return 0;
}
/*执行结果:
having used function()
deal2 process!!
deal1 process!!
*/
调用atexit注册时,会将“进程终止处理函数”的函数地址
压入进程栈中,当进程正常终止时,又会自动从栈中取出函数地址,并执行这个函数,实现进程的扫尾操作。
- 调用顺序刚好和注册顺序相反
- 在Linux下,调用atexit最多可以允许登记32个终止处理函数
- 同一个函数如果被登记多次,自然也会被调用多次
- 只有使用return和exit来正常终止时,才会调用
- 异常终止和_exit不会调用该进程终止处理函数
有时候需要在进程正常终止时,做一些扫尾操作
比如将链表数据保存到文件中
如果不使用进程终止处理函数的话,这个操作有点困难
- 因为进程有时候可能是因为某个函数调用失败,然后在函数出错处理时调用exit(-1)终止的,但是你又无法预估哪一个函数会出错,并在出错时调用相应的函数实现链表数据的保存.
- 这个时候就可以注册进程终止处理函数来实现了,因为进程终止时,会自动的调用终止处理函数来实现进程的扫尾处理--------将链表数据保存到文件中
