主要内容
①:unix/linux中main函数如何被调用
②:命令行参数如何传递给执行程序
③:典型存储器布局的样式
④:如何分配另外的存储空间
⑤:进程如何使用环境变量
⑥:各种不同的进程终止方式
⑦:longjmp和setjmp函数以及它们与栈的交互作用
⑧:进程的资源限制
1:main()函数
C程序总是从main()函数启动的,main()函数的原型是int main(int argc ,char *argv[]);
内核执行C程序时(使用一个exec),在调用main函数前先调用一个特殊的启动例程。可执行程序文件将启动例程指定为程序的起始地址。启动例程从内核获取命令行参数和环境变量值,为然后为按上述方式调用main函数做好准备.
2:进程的终止
UNIX/LINUX中有8种终止进程的方式,5种正常终止方式,3种异常终止方式,分别为:
正常方式:
①main()函数返回
②调用exit
③调用_exit或者_Exit
④最后一个线程从其启动例程返回
⑤最后一个线程调用pthread_exit
异常方式
⑥调用abort
⑦接到一个信号并终止
⑧最后一个线程对取消请求做出响应
上面提到了一个特殊的启动例程,如果用C语言的形式来表示(通常用汇编来写),它这么编写,exit(main(argc,argv));
3个正常终止程序函数的区别
①_exit()使用POSIX1.0说明的,头文件是unistd.h,exit()和_Exit()是由ISO C说明的头文件是stdlib.h
②_exit()和_Exit()立即进入内核,exit()会做清理工作(调用执行终止处理工作,关闭IO流)
atexit()函数
根据ISO规定,进程最多可以登记32个函数,这些函数由atexit()函数登记,由exit()调用,称为exit hander 终止处理程序.atexit原型如下
#include<stdlib.h>
int atexit(void (*func) (void));
参数是一个函数地址,这个地址的函数无传任何参数,也没有任何返回值.exit()调用它们的顺序,与登记的顺序相反.登记成功返回0,反之登记失败
3:环境表
每个程序都会有接收到一张环境表,类似于参数表,环境也是一个字符串指针数组,每个指针都是一个以NULL结尾的字符串地址.全局变量environ包含了环境指针的地址.
注意:通常通过getenv()和putenv来操作特定的环境变量,如果访问整个环境变量才使用environ指针.
4:C程序的存储空间布局
C程序的存储空间由以下及部分组成
①:正文段(Text segment),CPU执行的指令部分,这部分是可共享的,比如文本编辑器,shell,不过你打开多少个,正文段在存储器当中只需要一个副本即可.另外正文段常常是不可读,防止程序意外修改自身指令.
②:初始化数据段(Initialized data segment),通常也称之为数据段,包含程序当作需明确的赋初值的变量.例如C程序中,出现在任何函数之外的声明:
int maxcount = 100;
③:未初始化数据段(Uninitialized data segment),通常称为BSS段,在程序开始执行之前,由内核将此数据段中的数据初始化为0或者NULL.例如出现在任何函数之外的int arr[100];
④:栈(stack).自动变量以及每次函数调用时所需保留的信息都放在此段中.每次调用函数时,返回地址,调用者的环境信息(例如某些机器的寄存器值),新调用的函数在栈中申请一块空间,用来保存它的自动变量和临时变量。这也是为什C语言当中递归函数能够工作的原因,因为递归函数中每次它调用自身都会申请一个新的栈帧。因此同一个函数的两个或者多个调用实例不会互相影响。
⑤:堆(heap).用来动态分配内存.
一个典型的安排方式
unix/linux当中可以使用使用size命令查看正文段,数据段,以及bss段的大小
如下图所示
从上图中还可以看出,bss段的内容并不存放在磁盘文件当中.原因是内核再程序开始运行前会将它们都设置为0,存在磁盘的程序文件当中的只有正文段和初始化数据段.
text:正文段
data:初始化数据段
bss:未初始化数据段
dec和hex分别为三个数据段大小之和的十进制和十六进制表示.
5:共享库
又称为动态库,与之相反的是静态库。这种方法使得进程不需要包含公用的库例程,只在所有进程都可引用的存储位置维护这个库例程的一个副本即可。程序运行时再动态连接。
优点:
减少了可执行文件长度,库函数变更,不需要重新编译连接使用该库的程序。
缺点:
增加时间开销,这种开销发生在程序执行第一次调用共享库函数时。
example:
不使用共享库
使用共享库
可以看出不使用共享库的时候程序占用的空间变得很大
6:存储器分配
ISO三个用户存储器分配的函数
①void *malloc(size_t size);
分配一块指定字节数的存储区,初始值不固定
②void *calloc(size_t nobj , size_t size);
分配一块指定字节数的存储区,初始值为0
③void *realloc(void *ptr , size_t newsize);
更改以前分配的存储区的长度,如果长度增加,那么可能发生整个存储区域的移动,之前分配的内存里面值不变,而新增加的存储区值则不确定.另外由于可能发生存取区移动,所以对于任何以后可能需要用到realloc更改大小的存储区域,不应该有任何额外的指针指向其存储区中。newsize是指更改之后存储区的大小,而非前后之差。返回更改后的存储区指针。
注意:
a:三个函数返回的指针一定是适当对齐的,使其可用于任何数据对象。
b:大多数实现所分配的存储空间都比指定的大,额外空间用来记录管理信息。所以越界操作是件非常危险而且难以分析事情,尾端写入会重写后一个块的管理信息,起始位置之前写入,则会重写本块的管理记录。
其他替代的函数
libmalloc
vmalloc
快速适配(quit-fit)
alloca
这个函数在当前函数的栈帧上分配空间,函数返回时释放.
7:环境表
7.1:内核并不查看环境表,各个环境变量如何解释完全取决与应用程序
7.2父子进程环境表之间的关系
7.3:操作环境表的几个函数
char *getenv(const char *name); //获取某个环境变量的值,不存在则返回空
int putenv(char *str); //将取形式为name=value的字符串放到环境表中,如果环境表中以及存在name则将其替换
int setenv(const char *name,const char *value,const int rewrite) //将环境表中的name设置为value,如果name存在,则看rewrite,不为0替换,为0则
//不删除也不报错。
int unsetenv(const char *name); //删除name的定义,不存在也不报错
注意:setenv和putenv在实现时差别很大,putenv不会分配存储区,linux中将传递给它的字符串地址作为参数放入了环境表中,这种情况下如果将一个在栈中的字符串作为参数传递给了putenv就会发送错误,因为函数结束时,栈帧可能被重用。而setenv则必须分配存储区。
8:setjmp 和 longjmp
C语言当中go to并不能够跨越函数,但setjmp/longjmp能做到.这种方式用于深层次嵌套函数当中的出错处理非常有用。
常用方式
深层次嵌套函数调用当中的出错处理
jmp_buf jmpbuff //设置一个全局变量
setjmp(jmpbuff); //设置跳转位置
longjmp(jmpbuff,val) //执行跳转,val可以用来标记调用位置
9:getrlimit和setrlimit
9.1:每个进程均有一组资源限制,其中一些可由getrlimit和setrlimit查询或者更改
9.2:更改时遵循以下三条规则
①任何进程均可将其软限制更改为小于或者等于其硬限制
②任何进程均可降低其硬限制,但必须大于或者等于其软限制,普通用户而言这种修改是不可逆的
③只有超级用户可以提高其硬限制
9.3软硬限制
硬限制,软限制的上限,只能有超级用户提高
软限制,内核实际执行的限制,任何进程都可将其设为小于其硬限制的一个值
9.4 linux2.4.22支持的资源限制有如下几种
①RLIMIT_AS 进程可由存储区的最大长度,显然这会影响sbrk 和nmap函数
②RLIMIT_CORE core文件的最大字节数,为0则阻止创建core文件
③RLIMIT_CPU CPU时间的最大量值,超过此软限制时,向该进程发送SIGXCPU信号
④RLIMIT_DATA 数据段的最大字节长度.Initialized data segment,bss,heap的总和
⑤RLIMIT_FSIZE 创建文件的最大长度
⑥RLIMIT_LOCKS ...
⑦
P166