1、引言
2、main函数
3、进程的终止方式
4、exit和_exit函数
5、atexit函数
7、环境表
8、C程序程序空间布局
9、存储空间的手动分配
10、库文件
1、引言
一个人生活在某个地方,这个人一定有自己的周边生活环境,所谓生活环境,就是与人的生活息息相关的身边的人、事物、事情等等,那么一个进程也有它的运行环境,那么我们就称这个运行环境为进程环境,本章将重点讨论进程环境包含哪些东西,具体内容大致分为如下几个部分:
1)、main函数如何被调用的。
2)、内存布局的结构。
3)、如何在堆中分配空间。
4)、环境表和环境变量。
5)、库文件
等等。
2、main函数
2.1、main函数基本格式
1)、第一种标准格式
int main(void)
{
return 0;
}
2)、第二种标准格式
int main(int argc, char **argv) //char **argv等价于char *argv[]
{
return 0;
}
argc:argument count的缩写 ,存的是命令行输入的参数个数。
argv:argument vector的缩写,字符串数组,数组中每个元素用于存放命令行参数(字符串)地址。
平时我们看到的都是main函数调用子函数,但实际上main函数也是被别人调用的函数,被谁呢?被启动代码调用,所以第一种格式,启动代码调用main函数时无参数传递,第二种格式,有参数传递,传的是运行程序时从命令行输入的参数。
2.2、从命令行输入参数的例子
a.c
int main(int argc, char **argv) //char **argv等价于char *argv[]
{
int i = 0;
//打印出命令行参数的第一种方法
for(i=0; iprintf("%s ", argv[i]);
printf("\n");
//打印出命令行参数的第二种方法
for(i=0; NULL!=argv[i]; i++) printf("%s ", argv[i]);
printf("\n");
return 0;
}
上面程序的目的是打印出从命令行输入的命令行参数,打印的方法有两种,
第一种:靠argc(参数个数)进行控制打印结束。
第二种:靠(NULL!=argv[i])控制结束,因为字符串指针数组的最后位置(也就是argv[argc] 位置)会存入NULL表示结尾。
如运行./a.out a bb ccc ddd, 其打印结果如下:
./a.out a bb ccc ddd
./a.out a bb ccc ddd
两种方法均能正常运行,从结果我们看到,命令行参数的第一个参数,是可执行文件的名字,可能有些同学会觉得命令行参数没有用处,那就让我们来看看ls -a -l这个熟悉的命令,其中ls是可执行文件名,是第一个命令行参数,-l(列出文件的详细信息)和-a(.打头的隐藏文件也列出)被用于功能选择,是第二个和第三个命令行参数。
2.3、启动代码怎么生成的
启动代码是由gcc时,链接预定义的目标文件生成的,这里通过gcc -v打印的详细信息来看下具体的链接的过程,来看看gcc的链接时,用到了哪些预定义目标文件来生成启动代码。
以上面的a.c为例:
gcc -v a.c
然后将链接的详细过程摘录如下:
/usr/libexec/gcc/i686-redhat-linux/4.4.6/collect2 --eh-frame-hdr --build-id -m elf_i386 --hash-style=gnu -dynamic-linker /lib/ld-linux.so.2 /usr/lib/gcc/i686-redhat-linux/4.4.6/../../../crt1.o /usr/lib/gcc/i686-redhat-linux/4.4.6/../../../crti.o /usr/lib/gcc/i686-redhat-linux/4.4.6/crtbegin.o -L/usr/lib/gcc/i686-redhat-linux/4.4.6 -L/usr/lib/gcc/i686-redhat-linux/4.4.6 -L/usr/lib/gcc/i686-redhat-linux/4.4.6/../../.. /tmp/ccNJlcQu.o -lgcc --as-needed -lgcc_s --no-as-needed -lc -lgcc --as-needed -lgcc_s --no-as-needed /usr/lib/gcc/i686-redhat-linux/4.4.6/crtend.o /usr/lib/gcc/i686-redhat-linux/4.4.6/../../../crtn.o
/usr/libexec/gcc/i686-redhat-linux/4.4.6/collect2:链接命令;
-dynamic-linker:表明动态链接;
/lib/ld-linux.so.2:实现动态链接的库;
-L/usr/lib/gcc/i686-redhat-linux/4.4.6/../../.. /tmp/ccNJlcQu.o:临时生成的a.c对应的.o文件;
-lc:链接标准的动态库libc.so
/usr/lib/gcc/i686-redhat-linux/4.4.6/../../../crt1.o /usr/lib/gcc/i686-redhat-linux/4.4.6/../../../crti.o
/usr/lib/gcc/i686-redhat-linux/4.4.6/crtbegin.o /usr/lib/gcc/i686-redhat-linux/4.4.6/crtend.o
/usr/lib/gcc/i686-redhat-linux/4.4.6/../../../crtn.o:以上这些预定义的.o目标文件被用来专门生成启动代码,而main函数准确来说就是由crt1.o调用(其实crt1.o还留有动态库的接口,便于动态链接glibc动态库),main函数返回时会返回到启动代码。
启动代码传递给main函数的参数,则是其从内核获取的,而内核则是从命令行获取的,最后生成的可执行文件会将启动代码作为整个程序的入口,然后由启动代码自动调用main函数,从而使得整个程序得以运行。
以上只是简单的叙述了gcc的链接过程,主要因为这部分不属于系统编程的类容,详细情况请查阅有关gcc详解的资料。
3、进程的终止方式
3.1、正常终止
1)、从main函数返回(main函数调用return或不调用return的隐式返回);
2)、在程序的任意位置调用exit函数;
3)、在程序的任意位置调用_exit函数;
3.2、异常终止
1)、自杀:自己调用abort函数,自己给自己发一个SIGABRT信号将自己杀死,杀人凶器 是信号;
2)、他杀:由别人发一个信号,将其杀死,杀人凶器也是信号;
用c代码来模拟启动代码的话,其形式可认为是这样的:exit(main(argc, argv));,但实际上启动代码常常是用汇编写成的,启动代码包含了多种重要的初始化工作,不可能是如此简单的就能完成的,这里只是一种意会而已。
4、exit和_exit函数
4.1函数原型和所需头文件
#include
void _exit(int status);
#include
void exit(int status);
4.2、函数功能:
·_exit函数:正常终止进程,立即进入内核。
·exit函数:正常终止进程,然后调用终止处理函数和关闭打开的标准io流,最后再调用_exit
函数进入内核。
4.3、函数参数
·_exit函数的int status:指定退出状态
·exit函数int status:同上
4.4、函数返回值:均无。
4.5、注意
1)、_exit函数会立即进入内核。
2)、exit函数事先需要做一些处理,首先调用注册的进程终止处理函数,其次关闭所有打 开的标准io流,一旦标准io流被关闭,标准io的缓冲区会被立即刷新,之后才会进一步 的调用_exit函数进入内核。
这里提到的刷新一定是针对标准io的缓冲区,因为标准io缓冲区可能会积压数据,对于文件Io来说没有所谓刷新的问题的(因为对于文件io来说,一旦有数据立即操作,不会挤压)。
3)、_exit是真正的系统调用,而exit函数是一个库函数,它对_exit函数做了进一步的封装, 也就是说exit函数最终向下调用的还是_exit函数。
4)、如果出现以下情况时,该进程的终止状态是未定的。
a)、我们调用exit函数、_exit函数未写返回状态,如:exit()或则_exit();
b)、return时不带返回值;
c)、以上函数都未调用(这称为隐式返回);
以上会导致进程终止时,进程的终止状态是未定义的,所以我们认为上面的写法是非法的,要严格按照本章第2小节讲的main函数的两种标准格式来写,如果没有什么返回状态可返回的,我们一般返回0即可。可能有些同学想不通,进程的终止状态似乎没有什么用啊?我也从来没有过啊,那么对于这个问题我们在后面的章节将会讲到,这里先不关心这个问题。
4.6、测试用例:暂略
5、atexit函数
5.1函数原型和所需头文件
#include
int atexit(void (*function)(void));
5.2、函数功能:登记进程终止处理函数。
5.3、函数参数
·void (*function)(void):终止处理函数的地址。
5.4、函数返回值:函数调用成功返回0,失败返回非0值。
5.5、注意
1)、一个进程最多可以登记32个终止处理函数。
2)、这些函数由函数exit自动调用。
3)、注意终止处理函数的类型是void (*)(void)型的,没有参数,也没有返回值。
4)、登记时终止处理函数会被入栈,根据栈的特性,函数出栈的顺序与登记入栈的顺序相反。
5)、同一个函数如果被登记多次,则也被调用多次。
5.6、测试用例:暂略
6、c程序被启动和正常终止的过程
6.1、程序的启动和正常终止图示
内核执行一个程序的唯一方法是调用一个exec函数(我们在第8章将学习这个函数),而进程正常终止(这里强调的是正常终止)的方法是显示或隐式地调用exit函数或_exit函数,如果main函数中调用的是return,return返回到启动代码后,启动代码也会调用exit函数正常终止。
6.2、对exit,_exit和atexit函数的使用举例
6.2.1标准io中的行缓冲的缓冲区被刷新的条件
1)、行缓冲区满。
2)、遇到\n。
3)、显式或隐式地调用exit或return函数,通过前面的学习,我们已经知道,为什么调这两 个函数正常终止进程,行缓冲区会被刷新。
4)、显示调用fclose()函数关闭标准io流,这样行缓冲区也会被刷新。
5)、显示的调用fflsuh();刷新缓冲区函数。
6)、如果以上条件都不成立,但是在输出函数(puts除外,因为它会自动加一个\n进去) 的后面紧跟了一个输入函数的话,行缓冲区也会被刷新。
6.2.2、例子
本例子的目的是为大家演示return、exit、_exit、atexit的使用和区别。
/* 头文件省略 */
void exit_deal_fun1()
{
printf("1111111111");
}
void exit_deal_fun2()
{
printf("2222222222");
}
void exit_deal_fun3()
{
printf("3333333333");
}
int main(void)
{
atexit(exit_deal_fun1);
atexit(exit_deal_fun2);
atexit(exit_deal_fun3);
return 0;
//exit(0);
//_exit(0);
}
上例中,我们首先顺序的登记了exit_deal_fun1、exit_deal_fun2、exit_deal_fun3这三个进程终止处理函数,但是printf函数中的字符串都没有\n,所以行缓冲区不能按照\n的条件进行刷新的,进程终止方式有四种:
1)、return 0;
2)、隐式返回(等价于return 不带返回值的情况);
3)、exit(0);
4)、_exit(0);
根据不同的进程终止方式,分析下各自情况下的打印结果是怎样的。
1)、采用return 0:打印结果如下:
333333333322222222221111111111[linux@localhost xiangtan_1404]$
结果现象分析:
a)、结果还是被打印了出来,原因是return返回到启动代码后,启动代码会调用exit函 数终止进程,exit函数会自动关闭标准输出(stdout),这样一来缓冲区就得到了刷新。
b)、因为没有\n,所以输出结果全部连在了一起。
c)、打印结果的顺序与登记进程终止处理函数的顺序刚好相反。
2)、隐式返回:打印结果如下:
333333333322222222221111111111[linux@localhost xiangtan_1404]$
因为同上,因为也会返回到启动代码,由启动代码调用exit函数终止,只是这一次没有指明进程的终止状态。
3)、采用exit(0):打印结果如下:
333333333322222222221111111111[linux@localhost xiangtan_1404]$
打印结果也同上,但不会返回到启动代码,因为这一次我们是直接调用的exit函数。
4)、_exit(0):打印结果如下:
无输出结果。
产生这样结果的是因为,我们直接调用的是_exit函数,它是不会去调用终止处理函数的,当然也不会关闭标准Io流的,这里连终止处理函数都没有被调用,所以终止处理函数中的printf语句没有没调用,因此行缓冲区中实际上连数据都没有。
7、环境表
7.1、什么是环境表
1)、环境表同形参列表argv一样,其实也是一个字符串指针数组,其中数组中的每个元素 指向了以null结束的字符串地址。
2)、环境表包含的各个环境变量就是的字符串,这些字符串是对进程运行环境的说明,程序
运行时依赖于张环境表,比如我们进程所在的当前工作目录就记录在了环境表中PWD环境变 量中。当前进程收到环境表是继承自当前进程的父进程的环境表(第9章讲父进程这个概念)。
3)、环境表中存放的各种环境变量是在系统启动时,读取自于各种存放环境变量的文件得到 的,我们的环境表存放在了当前进程的虚拟内存中。
7.2、shell终端进程的环境表
7.2.1、查看shell终端进程的环境变量
1)、查看shell终端环境表中所有的环境变量
执行命令export,我的shell终端环境表如下:
。。。。。。
declare -x GDM_KEYBOARD_LAYOUT="us"//键盘格式
declare -x GDM_LANG="en_US.utf8" //中文的编码格式
。。。。。。
declare -x HISTSIZE="1000" //历史命令条数
declare -x HOME="/home/linux" //主目录路径
declare -x HOSTNAME="localhost.localdomain" //主机名称
。。。。。。
declare -x LOGNAME="linux" //登录名
。。。。。。
/* ls显示文件时的颜色说明 */
LS_COLORS="rs=0:di=01;34:ln=01;36:mh=00:pi=40;33:so=01;35:do=01;35:bd=40;33;01:cd=40;33;01:or=40;31;01:mi=01;05;37;41:su=37;41:sg=30;43:ca=30;41:tw=30;42:ow=34;42:st=37;44:ex=01;32:*.tar=01;31:*.tgz=01;31:*.arj=01;31:*.taz=01;31:*.lzh=01;31:*.lzma=01;31:*.tlz=01;31:*.txz=01;31:*.
。。。。。
35:*.fli=01;35:*.flv=01;35:*.gl=01;35:*.dl=01;35:*.xcf=01;35:*.xwd=01;35:*.yuv=01;35:*.cgm=01;35:*.mid=01;36:*.midi=01;36:*.mka=01;36:*.mp3=01;36:*.mpc=01;36:*.ogg=01;36:*.ra=01;36:*.wav=01;36:*.axa=01;36:*.oga=01;36:*.spx=01;36:*.xspf=01;36:"
。。。。。。
/* 各种可执行文件或库等路标文件所在的路径,路径之间用:隔开 */
PATH="/usr/lib/qt-3.3/bin:/usr/kerberos/sbin:/usr/kerberos/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/FriendlyARM/toolschain/4.4.3/bin:/opt/arm_gcc/crosstool/gcc-3.4.5-glibc-2.3.6/arm-linux-gnu/bin:/home/linux/bin:/opt/arm_gcc/crosstool/gcc-3.4.5-glibc-2.3.6/arm-linux-gnu/bin"
declare -x PWD="/home/linux/shang_qian/xiangtan_1404"
.。。。。。。
declare -x SHELL="/bin/bash" //shell版本
。。。。。。
等号前面的是环境变量的名称,一般为大写,等号的后面是具体的环境变量,是一个字符串,字符串具体代表的含义由使用该环境变量的程序自行解释。
这张环境表中的PATH这个环境变量尤为重要,包含了各种的可执行文件和库等目标文件所在的路径,各个路径之间用:隔开,比如ls(命令)这个可执行文件存放在了/bin这个目录下,我们能够在PATH这个环境变量中找到/bin这个目录,所以当我们运行ls时,不需要指定ls所在的路径,会自动到环境表中的PATH环境变量中的各个路径下找,找到了就执行,没有找到就报错。
2)、查看某个具体的环境变量,如:
执行命令export $PATH,打印结果如下:
bash:export:`/usr/lib/qt-3.3/bin:/usr/kerberos/sbin:/usr/kerberos/bin:/usr/local/bin:/usr/ bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/FriendlyARM/toolschain/4.4.3/bin:/opt/arm_ gcc/crosstool/gcc-3.4.5-glibc-2.3.6/arm-linux-gnu/bin:/home/linux/bin:/opt/arm_gcc/cros stool/gcc-3.4.5-glibc-2.3.6/arm-linux-gnu/bin': not a valid identifier
或者执行echo $PATH,打印结果如下:
/usr/lib/qt-3.3/bin:/usr/kerberos/sbin:/usr/kerberos/bin:/usr/local/bin:/usr/bin:/bin:/usr/ local/sbin:/usr/sbin:/sbin:/opt/FriendlyARM/toolschain/4.4.3/bin:/opt/arm_gcc/crosstool /gcc-3.4.5-glibc-2.3.6/arm-linux-gnu/bin:/home/linux/bin:/opt/arm_gcc/crosstool/gcc-3.4. 5-glibc-2.3.6/arm-linux-gnu/bin
以上两种情况打印结果只是略有区别而已。
7.2.2、用命令,添加或修改shell终端进程的的环境变量
1)、添加一个新的环境变量
export AA=aaaaaaaaaa
这里的字符串可加” ”,也可不加,我们执行export命令查看下,可以发现环境变量表中多了一个叫AA的环境变量,如果以前已经有了一个叫AA名字的环境变量,那么这一次会无条件覆盖前一次的内容。
2)、修改原有的环境变量。
比如将当前路径加入到PATH环境变量中,执行命令入下:
export PATH=$PATH:`pwd`
` `的意思是将``里面的输出结果作为前面export PATH=$PATH:的输入,当我们export $PATH查看结果如下:
/usr/lib/qt-3.3/bin:/usr/kerberos/sbin:/usr/kerberos/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin:/opt/FriendlyARM/toolschain/4.4.3/bin:/opt/arm_gcc/crosstool/gcc-3.4.5-glibc-2.3.6/arm-linux-gnu/bin:/home/linux/bin:/opt/arm_gcc/crosstool/gcc-3.4.5-glibc-2.3.6/arm-linux-gnu/bin:/home/linux/shang_qian/xiangtan_1404
发现我的当前路径/home/linux/shang_qian/xiangtan_1404加入到了PATH中,这么一来,当在任何路径下运行存放该路径下的可执行文件时,并且不必指定可执行文件的所在路径,因为当我们不指定可执行文件的路径时,它会自动到PATH中的各个路径下去找这个可执行文件,找到运行,未找到报错。
7.2.3、删除shell终端环境变量:使用unset命令,如unset PATH
7.2.4、注意
我们通过export去修改环境表时,是临时的修该,当我们关掉shell终端时,环境表的设置又恢复为默认的情况。如果我们想永久的修改环境变量的话,我们必须修改环境变量文件才行,这里略去讲解。
7.3、进程的环境表
实际上一个shell终端被打开,它也是一个运行起来的进程,但是我们一般是通过命令来显示、修改、添加和删除环境变量,对于这些在前面我们已经讲解完毕。
对于我们./a.out运行的一个程序来说,它也会有一张环境表,这张环境表是继承自运行改该程序的shell终端进程的。对于这a.out的环境表我们也可以实现显示、修改、添加和删除环境变量等操作,但是都是通过函数来实现。
7.3.1、显示进程的环境表
1)、方法一:通过全局变量environ实现,例子代码如下:
int main(void)
{
extern char **environ;
int i = 0;
while(1) {
if(NULL == environ[i]) break;
printf("%s\n", environ[i]);
i++;
}
return 0;
}
程序运行的结果和export查询的结果是一致的,因为当前进程的环境就是表继承自运行改程序的shell终端进程的进程表。
2)、方法二:利用main函数的第三个参数实现
大多数同学认为,传给main函数的参数就只有两个,但是实际上还有第三个参数,这第三个参数就与环境变量有关,例子代码如下:
int main(int argc, char **argv, char **env) //char **env等价于char *env[]
{
int i = 0;
while(1)
{
if(NULL == env[i]) break;
printf("%s\n", env[i]);
i++;
}
return 0;
}
运行结果与上例同。
7.3.2、获取指定的环境变量函数,getenv
1)、函数原型和所需头文件
#include
char *getenv(const char *name);
2)、函数功能:按照name指定的名字搜索进程环境表,找到该名字对应的环境变量。
3)、函数参数:name,需要被查找的环境变量的名字。
4)、函数返回值:调用成功返回环境变量内容(字符串)的首地址,失败返回NULL。
5)、注意:
a)、返回的只是环境变量的内容,而不包含环境变量的名字。
b)、该函数是按照环境变量名字精确搜索。
6)、使用例程
int main(void)
{
char *p = NULL;
p = getenv("PATH");
printf("%s\n", p);
return 0;
}
7.3.3、设置环境变量函数,putenv、setenv
1)、函数原型和所需头文件
#include
int putenv(char *string);
int setenv(const char *name, const char *value, int overwrite);
2)、函数功能
putenv函数:设置新的环境变量到环境表中。
setenv函数:功能同上。
3)、函数参数:
·putenv函数的char *string:新的环境变量,同时包含名字和内容(name=value)。
·setenv函数
const char *name:环境变量的名字。
const char *value:环境变量值。
int overwrite:如果name名字的环境变量以前已经存在,问现在现在是否覆盖原 有的环境变量,写0代表不覆盖,本次设置无效,!0则覆盖。
4)、函数返回值
·putenv函数:调用成功返回0,失败返回非0。
·setenv函数:调用成功返回0,失败返回-1。
5)、注意:
a)、使用putenv设置时,如果环境表中已经存在该名称的环境变量,那么本次设置
会无条件的覆盖之前的环境变量值。
b)、当使用setenv设置时,如果环境表中已经存在该名称的环境变量,那么是否覆 盖之前的环境变量值是可供选择的。
6)、使用例程
int main(void)
{
char *p = NULL;
/* putenv */
putenv("AA=bbbbbbbbbbbbbb");
p = getenv("AA");
printf("%s\n", p);
/* setenv */
setenv("BB", "bbbbbbbbbbbbbb", 0);
p = getenv("BB");
printf("%s\n", p);
return 0;
}
7.3.4、删除环境变量函数unsetenv,clearenv
1)、函数原型和所需头文件
#include
int unsetenv(const char *name);
2)、函数功能
unsetenv函数:删除name指定名字的环境变量。
3)、函数参数:char *name:环境变量名。
4)、函数返回值:unsetenv函数:函数调用成功返回0,失败返回非0。
5)、注意:当unsetenv删除name的环境变量时,即使不存在也不能算错。
6)、测试用例:如unsetenv("PATH");
9、C程序程序空间布局
8.1、c程序(c的可执行文)结构
一般来说我们的c可执行文件,包含如下几部分。
1)、正文段:也可称为代码段或代码节,该段特点如下:
·此段为共享段;
·运行时,内存会复制一份该段的副本;
·为只读,目的是为了方式意外情况将代码修改而导致程序出错;
2)、数据段:数据段包含如下3个节
a)、ro.data:常量数据节,该节特点如下:
·存放常量数据,如printf(“%s %d”, xx, xx);和char *p = “hello”中的常量字符串就 存在了该数据节中;
·为只读;
b)、.data:数据节,该节特点如下:
·专门用于存放初始化了的静态变量。当可执行文件在磁盘上时,这个节是要开出开
出空间来存放用于初始化的常量数值的;
·为可读可写;
c)、.bss:也是一个数据节,其特点如下:
·存放未初始化了的静态变量。当可执行文件在磁盘上时,这个节是没有开出变量空 间的,因为没有设置初始化常量数据,这里只有一些占位符起说明所用,当程序运行
起来后,在内存中会为该节开出空间,并且会全部清0;
·为可读可写;
3)、其它段或节:如果我们的程序是在操作系统上运行的话,还用该有其它的段或节,目的
是为了辅我们的c程序在操作系统上运行起来。
8.2、c程序运行空间(虚拟内存空间或进程空间)
8.2.1、虚拟内存空间
我们这里讲c程序的运行,指的都是基于操作系统的,而不是在裸机上,所以使用的内存是虚拟内存上的,因此c程序运行空间也可称为虚拟内存空间,也可称为进程空间,因为每个进程拥有独立的虚拟内存空间。
1)、为什么需要虚拟内存
虚拟内存空间是一个虚拟的概念,由于早起软件的发展要快于物理内存的发展,导致物理内存不够用,再加上物理内存的造价高昂,单方面的采用扩充物理内存的方法是不现实的,最后我们通过虚拟内存机制解决了这矛盾,以32位机来说,虽然物理内存可能只有64M,但是虚拟内存承诺给每个进程4G的内存空间,解决了内存不足的问题,我们完全可将这4G的虚拟内存空间等价的看作是4G的物理空间,性能上是基本一致的。
2)、虚拟内存如何构成
虚拟内存是由硬件和软件各自捐献功能相互组合起来的这么一个虚拟功能,硬件诸如MMU(内存管理单元),物理内存,磁盘,多级TLB(页表缓存),多级cache(数据和指令缓存),软件如内核的内存管理代码等。
做个比喻,如国家的这个概念,其实也是一个虚拟的概念,看不见摸不着,但是它是由人民,民族,国土面积,人口等的实际的东西组合起来的,当我们说等到国家时想到的是这些实际的概念,虚拟内存就类似于这么一个东西。
但是我们的程序本质上还是运行在我们的物理内存中,但是程序计数器pc存的却是虚拟地址,我们为了访存实际物理内存上的数据和代码,虚拟地址最终需要被转为物理地址,转换功能由MMU实现,但是MMU需要查找虚拟与物理地址的对照表才能实现,而这张对照表存在了TLB中(这里可以简单的这么认为)。
3)、虚拟内存的好处
a)、解决了物理内存不足的问题。
b)、为实现多进程提供了基础,因为可以为每一个进程提供一个独立的虚拟内存。
c)、给编译器带来了便利,编译时都将程序的首地址重定位到虚拟内存中的同一个 位置即可,因为每个进程有一个自己独立的内存空间,所有的定位到同一个位置 并不会互相冲突。
d)、极大地提高了物理内存的使用效率。
8.2.2、要虚拟内存空间的布局
虚拟内存空间的布局在结构上基本与我们前面讲的c程序的结构一致,为了好讲解,我们这里以32位机为例,所以对于32位机来说,虚拟内存的最大内存空间为2的32次方(==4G),布局结构如下图:
从上图我们看到,虚拟内存空间的大小是4G(虚拟地址从0到4G-1),对于上面同一个内存结构,我们以不同的眼光看待时,有不同的划分方法。
1)、第一种划分法:分为应用空间和内核空间
·应用空间:0~3G,用于运行应用代码,其中0~0x08048000之间的空间未用。
·内核空间:3~4G,用于运行系统调用函数的内核级的代码。
这两部分空间是有权限差别的,应用空间不可以访问内核空间,但是反过来在遵循某些条件的情况下,内核空间是可以访问应用空间的。
进行系统调用时,从应用空间陷入内核空间(采用swp软中断机制),系统调用完成后又从内核空间返回应用空间,内核空间能够无条件的访问各种寄存器和内存,但是应用空间却除了访问前面说的应用空间的内存外,对其它的无能为力。
2)、第二种划分方法:将应用空间分为代码段和数据段
·代码段:只包含.text
·数据段:包括ro.data,.data,.bss,堆和栈
3)、第三种划分法:将应用空间分为静态数据区和动态数据区
·静态数据区:包含.text,.ro.data,.data,.bss,静态数据区的空间分配是由编译的时候
决定的,运行时不可以在静态去开空间,也不可以将静态区中已开出的空间释放。
·动态数据区:包含栈和堆
栈:自动区,栈中空间在程序运行时,自动的开出和自动释放。
堆:手动区,堆中空间在程序运行时,手动开出(malloc等函数)和手动的释放(free 函数),如果程序结束时没有释放开出的空间的话,这些空间会被一直占用,导 致内存泄露,直到机器从新启动后,这些堆中的空间才会被释放。
4)、第四种划分法:将应用空间分为只读段和可读可写段
·只读段:包含.text.,ro.data
·可读可写段:包含.data,.bss,堆,栈
9、存储空间的手动分配
9.1、手动在堆中分配,涉及函数malloc,calloc,realloc,free
9.1.1函数原型和所需头文件
#include
void *malloc(size_t size);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
void free(void *ptr);
9.1.2、函数功能:
·malloc函数:从堆中分配出size字节的空间。
·calloc函数:从堆中分配出nmemb*size字节的空间。
·realloc函数:更改以前原有分配空间的大小,新空间大小可以增加也可减少。
1)、新空间大小空间减少:将原有空间截断(多余空间会被释放掉),返回传给它的
同样的指针值(原空间地址),但是这样可能会导致原有空间的部分数据丢失(因为原有空间中数据过长的话,可能会被截掉)。
2)、新空间大小增加:这是比较常见的用法,增加空间的实现主要有如下两种方法。
a)、如果在的原有空间的后面有足够的空间可用,则在原存储区位置向高地址
方向扩充,并且返回传给它的同样的指针值(原空间地址)。
b)、如果在原有空间的后面没有足够的空间可用,则realloc分配另外一个足够
大的区域,并且将原有空间的内容复制到新的新的空间,并且将原有空
间释放,这时返回的新空间的地址和原有空间的地址是不同的。
·free函数:释放从堆中开出的空间。
9.1.3、函数参数
·malloc函数的size_t size:指定空间大小。
·calloc函数
ize_t nmemb:块数
size_t size:每一块的大小
·realloc函数
void *ptr:指向原有空间。
size_t size:新空间的大小。
·free函数的void *ptr:指向需要释放的空间。
9.1.4、函数返回值
·对于malloc,calloc,realloc,这三个函数,调用成功返回新分配空间的首字节地址, 失败返回NULL,并且errno被设置。
·free函数:无返回值。
9.1.5、注意
1)、指定空间大小时,是以字节为单位。
2)、从堆中开出的空间必须free,否则会造成内存泄露。
3)、对于开出的同一片空间不能多次free(只能free一次),多次free会导致出错。
4)、对于realloc来说,原有空间的指针不要去free(系统会帮我们处理好的),我们只 需要free掉realloc新返回的指针即可。当realloc的函数的ptr==NULL时,功能同malloc。
5)、一般来说实际分配的空间会略大于我们要求的空间,因为我们所要求的空间实际上 是有很多不连续的分配块(当然块内是连续的)组成的,所以每个分配块会被分配额外 的空间用来进行分配块块的记录和管理(如记录下当前分配块的长度,和包含指向下一 个分配块的指针等)。
9.1.6、使用例程
int main(void)
{
char *p1 = NULL, *p2 = NULL, *p3 = NULL;
/* 1. malloc */
p1 = malloc(10);
strcpy(p1, "hello");
printf("p1 = %s\n", p1);
free(p1);
/* 2. calloc */
p2 = calloc(5, 2);
strcpy(p1, "hello");
printf("p2 = %s\n", p1);
free(p2);
/* 3. realloc */
char *new_p = NULL;
new_p = malloc(1);
strcpy(new_p, "hello");
p3 = realloc(new_p, 10);//将new_p指向的内容复制到普新开得空间
printf("p3 = %s\n", p3);
free(p3);
return 0;
}
9.1、手动在栈中中分配,涉及函数alloca
我们以前讲过栈是自动区,自动局部变量在栈中自动的开出空间和释放空间,但是实际上我们也可以用alloca函数手动的在栈中开出空间,但是由于栈能够自动的释放空间,所以这里并不需要调用free函数释放在栈中开出的空间。
9.1.1函数原型和所需头文件
#include
void *alloca(size_t size);
9.1.2、函数功能:从栈中分配出size字节的空间。
9.1.3、函数参数:指定需分配的空间大小。
9.1.4、函数返回值:成功返回分配空间的首字节地址,失败返回NULL,并且errno被设置。
9.1.5、注意:开出的空间会被自动释放,不能free,如果free会导致错误。
9.1.6、使用例程
int main(void)
{
char *p = NULL;
p = alloca(10);
strcpy(p, "hello\n");
printf("p = %s\n", p);
return 0;
}
10、库文件
10.1、为什么需要库
早期其实是没有库的,比如我们想实现一个开方的运算,就必须自己实现这个算法,这是很麻烦的事情(可能连数学都还没学好呢!!),但是假如有人帮我们早就实现好了的话,我们直接调用有多好。
库就像是一个仓库,里面存放了非常多的早已写好的现成工具函数,库就是一堆的.o的目标文件的集合(二进制的),我们想调用库函数时,只需在编译时链接上库中的被调函数所在的.o文件即可。
10.2、库有静态库和动态库
1)、静态库:特点如下。
·链接静态库时,相当于直接复制库中我们需要的.o文件代码到可执行文件中。
·链接静态库后,可执行文件代码量会增加。
·如果有很多人都会用到这个静态库的话,每个人都会复制一份。
·静态库文件的后缀是****.a。
2)、动态库:也称为共享库,特点如下。
·链接时动态库相关代码不会被直接复制到可执行文件中,但是在启动代码中会留下接
口。
·链接完动态库后,可执行文件的代码量不会增加。
·如果很多程序都在使用同一个动态库的话,在内存中大家共享同一份动态库代码副本。
·动态库文件的后缀****.so,如果.so后面还有其他后缀指的是库的版本号。
3)、静态耗内存,但是省时间,动态库省内存但是耗时间,不论是静态库还是动态库,起名 是都要以lib作为头缀,然后是库名称,再接上各自的后缀。如lib+库命+.+后缀,比如libm.so,
m才是真正动态库的名字。
10.2、静态库
10.2.1、如何做一个简单的静态库
本静态库的功能是实现两个数的加法和一个数的整数次幂的运算。
1)、静态库的c文件
my_add.c:实现两个数相加
double my_add_fun(double add1, double add2)
{
return add1 + add2;
}
my_power.c:实现幂次运算、
double my_power_fun(double x, double n)
{
int i = 0;
double sum = 1, power = -1;
if(n < 0) power = -n;
else power = n;
for(i=0; isum *= x; //实现累乘
if(n < 0) sum = 1/sum;
return sum;
}
2)、写出静态库的头文件
这个头文件中主要是为了对静态库中的函数、使用的宏和一些类型做好声明,以便静态库的使用者调用。
my_static_add_power.h
#ifndef H_MY_STA_LIB_H
#define H_MY_STA_LIB_H
extern double my_add_fun(double add1, double add2);
extern double my_power_fun(double x, double n);
#endif
3)、将.c生成静态库,分如下两部进行
a)、首先生成.o文件
gcc -c my_add.c -o my_add1.o
gcc -c my_power.c -o my_add1.o
b)、最后将一堆的.o文件做成静态库文件
ar crs libmy_static_add.a my_add1.o my_add2.o
10.2.2、如何使用(链接)静态库
1)、写出我的应用代码
my.c
#include
#include "my_static_add_power.h"//包含静态库的头文件
int main(void)
{
double a = 100, b = 200, sum1 = 0;
double x = 2, power = 4, sum2 = 0;;
sum1 = my_add_fun(a, b); //调静态库中实现的函数,实现两个数相加
printf("sum = %lf\n", sum1);
sum2 = my_power_fun(x, power);//调静态库中实现的函数,实现x的power次幂
printf("sum2 = %lf\n", sum2);
return 0;
}
2)、将我的应用程序和静态库一起连接
a)、方法一:gcc a.c ./libmy_static.a
b)、方法二:gcc add.c -L. -lmy_static_add(-L.:库在.目录下)
c)、方法三:首先cp libmy_static_add.a /lib //此操作需要超级权限,/lib被加入PATH
然后gcc add.c -lmy_static_add
实际上这三种方法完全是一样的。
·第一种:直接指明静态库路径名,这个路径名既包含了静态库所在的目录,包含了
具体是静态库名称(包含lib头缀和.a尾缀),如上面的./libmy_static.a,说明是当前
路径下的libmy_static.a。
·第二种:用-L加静态库所在的目录的路径来指明静态库所在的位置,-l加上静态库
的名(去掉头缀lib和.a就是静态库的名称)来指定具体的静态库。
·第三种:这种方法中我们将静态库移到了/lib目录下,由于/lib这个路径已经加入 了环境变量PATH中,所以我们链接该静态库时是不需要指定库的所在目录的路径的, 因为系统会到PATH环境变量中指定的路径下搜索,只需要-l指定具体的静态库就行。
10.3、动态库
10.3.1、如何做一个简单的动态库
1)、动态库的c文件还是用前面的my_add.c,my_power.c。
2)、动态库的头文件:哈使用my_static_add_power.h。
3)、将c文件做成动态库
·方法一:
a)、首先将.c生成.o文件
gcc -c my_add.c -o my_add1.o
gcc -c my_power.c -o my_add1.o
b)、最后将一堆的.o文件做成动态库文件
gcc --shared my_add.o my_power.o -o libmy_dynamic.so
--shared :代表生成动态库。
·方法二:gcc --shared my_add.c my_power.c -o libmy_dynamic.so
以上两种方法是一样的,第一种中.o文件是自己预先gcc*得到的,第二种方法中的.o是由gcc生成临时的。
10.3.1、使用(链接)动态库
1)、cp libmy_dynamic.so /lib //超级权限下操作
2)、gcc a.c -lmy_dynamic -o exe
如果gcc a.c libmy_dynamic.so -o exe,这实际上静态链接动态库,你把一个动态库当静态库在用。
这里必须将动态库加入/lib(/lib在PATH中)下,或者将动态库当前所在的目录路径加入环境变量PATH中,否者即便是我们在gcc链接动态库时,-L.指定了动态库所在的目录而链接成功,但是但是动态库的代码不会被复制到可执行文件中,因为动态链接只会留下接口,在实际运行动态库时才会根据接口从PATH指定的路径中找到动态库代码并复制一份到内存中运行,如果动态库所在的路径没有加入PATH环境变量的话,动态运行时就搜索不到了,所以动态库所在目录必须加入环境变量PATH中,如果其它程序也在用这个动态库的话,将会共享刚才复制到内存中相同的那一份,这样会节省内存,但是比较耗时间。
10.4、大家对于库可能存在的疑惑
从上面的例子中,可能有些同学觉得,这个库搞得挺麻烦,但是似乎每什么用处啊,主要因为这里只简单的举个例子为大家做说明而已,你会觉得没什么用,但是以后随着库的功能的增多,用处和方便度会越来越明显,目前但是可能大家有如下疑惑,我们来看看。
·疑惑一:不用库行不行?
答:理论上说肯定是可以的,但是如简单的printf函数(我们平时只要写一句话就搞定),它是由动态库实现的,如果没有动态库的话,难道我们每次写代码都需要自己去从头实现一遍printf函数吗,这是不可能的,更何况可能由于平台的不同,同样是printf函数,不同平台下具体实现方法却有所不同,对于初学者或项目开发者来说,牛人们帮我们写出,我们拿来即可用是最好了,并且我们也没有这个时间、经历或能力去实现这些东西。
·疑惑二:能不能将库直接直接一个.o来实现实现就好了?
答:原则上是可以的,但是我们使用时只是用到了其中一两个函数,这会导致:
如果是静态库的话,整个.o都会被复制到可执行文件中,代码变得庞大,运行时非常的很耗内存。
如果动态库的话,虽然大家共享复制到内存中的副本,但是这个副本也会很大,包含了很多无用代码,也很耗内存。
分成多个.o文件,每个.o具有很强的功能内聚性,某个.o专门用于完成某个功能,链接时只链接所需要的.o即可,大大的节省了内存空间。
·疑惑三:为什么要将很多的.o做成.a或.so,是不是多次一举呢?
答:如果这些.o没有用库进行管理的话,链接时就要自己具体的指定需要被使用的某个.o,而这些.o可能成百上千个,我们都要记住名字的话,这将会是一件很痛苦的事情。而每个.a或.so就像一仓库一样,里面放的各个.o就像是各种各样工具一样,当我们的应用代码中用到了某个库函数,链接器会自动跑到我们指定的库中的每个.o中搜索这个函数,找到后,就将拥有这个函数的.o链接进来,而其它.o不会被链接,这就避免了需要记住成百上千.o文件名字的痛苦的发生。
·疑惑四:为什么需要头文件?
有些同学可能疑惑为什么需要库头文件呢?比如你使用printf函数,为什么要包含stdio.h头文件是一样的道理,头文件中对库中的函数做了声明,同时定义了各种有用的宏和类型,没有这些东西库是不完整的,我们必须包含这些头文件才能正常的使用某个库。
·用到了某个库函数,虽然链接了库,但是没有包含头库文件,这种情况下会有警告(警告库函数没有声明,或称为函数是隐式声明的)但不报错,因为链接时最终还是链接到了我们需要的库函数,这种情况一般是不会有问题的,但是当需要用到库函数的返回值时,如果没有对库函数进行声明的话(库函数的声明放在了头文件中,但是这里没有包含该库的头文件),可能会导致返回的返回值发生误错,可能因为类型不对导致的。
·如果我们用到了某个宏,如NULL这个常用宏,它被定义在了stdio.h这个头文件中,但是如果没有包含这个头文件的话,是会报错的,因为不包含这头文件,这个NULL得不到宏替换,编译时会认为NULL是一个不认识的标识符。
·如果我们用到了某种类型,而这个类型又定义在了头文件中,如FILE这个类型定义在了stdio.h中,那么我们必须包含这个头文件,否者使用FILE类型时,编译器会因为不认识FILE而报错。
综上所述,使用库函数,我们一定要包含相应的头文件,头文件最好都放在这个程序文件的最前面,如果放在了程序文件的后面,函数就用不到头文件中的内容(类似于变量的作用域的概念),如果实在是理解不了库的头文件是用来干什么的,你就好好想想你自己写的头文件都是用来干什么的,这就很清楚了。
10.5、库的好处如下:
1)、为实现语言的跨平台使用提供了可能:
同样的printf函数,但在windows平台下写的c代码调用这个函数和linux平台下写的c代码调用这个函数时,由于os平台的不同,同样名字函数的具体实现也是大不相同的,具体不同就由各自的不同库来实现,但是应用代码的调用的接口都称为printf,这样一来c代码既可以在windows也可以在linux运行,只需链接不
同的库就行,这使得语言变得更加的强大。
2)、项目的开发和功能提升更为容易:
正是由于个各种现成库可以使用,使得在开发项目时这些库极大的减少了开发的工作量,当并且我们的项目想要功能升级时,我们完全可以用库插件的形式来时升级项目功能。
3)、库方便维护和升级。
4)、极大的提高了不同研发团队之间的代码的共享的可能。