引言
- 当程序执行时,其main函数是如何被调用的–内核调用启动例程执行exec函数再执行main函数内容。
- 命令行参数是如何传递给新程序的–调用exec的进程可将命令行参数传递给该新程序
- 典型的存储空间布局是什么样式
- 如何分配另外的存储空间
- 进程如何使用环境变量
- 进程的各种不同终止方式
- longjmp和setjmp以及它们与栈的交互作用
- 查看进程的资源限制
main函数
int main(int argc, char *argv[]);
C程序总是从main函数开始执行,argc是命令行参数的数目,argv是指向参数的各个指针所构成的数组,也就是各个参数的地址。
当内核执行C程序时,在调用main函数之前先调用一个特殊的启动例程。启动例程从内核取得命令行参数和环境变量值。
进程终止
正常终止
- 从main返回
- 调用exit
- 调用_exit或_Exit
- 最后一个线程从其启动例程返回
- 从最后一个线程调用pthread_exit
异常终止
- 调用abort
- 接到一个信号
- 最后一个线程对取消请求做出相应
退出函数
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);
参数status称为终止状态,如果main函数中没有声明返回类型为整型,那么该进程的终止状态是未定义的。例如
main(){
printf("kk");
}
终止状态是未定义的。而又如
int main(){
printf("kk");
return 0;
}
则该进程的终止状态为0。
exit函数总是执行一个标准I/O库的清理关闭操作:对于所有的打开流调用fclose函数–》这意味着exit将冲洗缓冲的数据,使得内核中数据悉数写入磁盘文件中。其实在main函数exit(0)和return(0)是等价的
函数atexit
一个进程可以登记多至32个函数,这些函数将由exit自动调用。我们将这些函数称为终止处理程序,并调用atexit来登记这些函数。
#include <stdlib.h>
int atexit(void (*func)(void));
//参数为一个函数地址
atexit函数调用被登记的函数时,既不需要给被调用的函数传递一个参数,也不期望被调用的函数返回一个值。注意:atexit登记终止处理函数的顺序与它调用终止处理函数的顺序相反。
atexit函数的实例如下:
#include "apue.h"
static void my_exit1(void);
static void my_exit2(void);
int
main(void){
if(atexit(my_exit2) != 0)
err_sys("can't register my_exit2"); //登记终止处理程序my_exit2
if(atexit(my_exit1) != 0)
err_sys("can't register my_exit1");
if(atexit(my_exit1) != 0)
err_sys("can't register my_exit1");
printf("main is done\n");
return 0;
}
static void my_exit1(void){
printf("first exit handler\n");
}
static void my_exit2(void){
printf("second exit handler\n");
}
结果:
main is done
first exit handler
first exit handler
second exit handle
命令行参数
当执行一个程序时,调用exec的进程可将命令行参数传递给该新程序。
环境表
每个程序都接收到一张环境表,与参数表一样,环境表也是一个字符指针数组,其中每个指针包含一个以NULL结尾的C字符串的地址。
环境指针–环境表–表项为环境字符串(环境变量)
int main(int argc, char **argv, char **envp);
//envp就是环境表地址
C程序的存储空间布局
- 正文段:由CPU执行的机器指令部分。通常,正文段是共享的,而且常常是只读的,防止程序由于意外而修改其指令。
- 初始化数据段:包含程序中需明确地赋初值的变量。例如int maxcount = 9,便使此变量以其初值存储在初始化数据段中。
- 未初始化数据段:在程序开始执行之前,内核将此段中的内容初始化为0或空指针:long sum[100],使次变量存储在未初始化数据段中。
- 栈:存放自动变量以及每次函数调用时所需保存的信息。每次函数调用时,其返回地址以及调用者的环境信息都存放在栈中。
- 堆:通常在堆中进行动态存储分配。堆位于未初始化数据段与栈之间。
共享库
1.共享库使得可执行文件中不再需要包含公用的库函数,而只需在所有进程都可引用的存储区中保存这种库例程的一个副本。程序第一次执行或者第一次调用某个库函数时,用动态链接方法将程序与共享库函数相链接–>减少了可执行文件的长度。
2.库函数进行更新之后,无需对使用该库的程序重新连接编辑。
存储空间分配
存储空间动态分配的函数
#include <stdlib.h>
void *malloc(ssize_t size); //分配指定字节数的存储区,此存储区中初值不确定
void *calloc(ssize_t nobj, size_t size);//为指定数量指定长度的对象分配存储空间,该空间的每一位(bit)都初始化为0
void *realloc(void *ptr, size_t newsize);//增加或减少以前分配区的长度
void free(void *ptr);
这些分配内存的例程通常用sbrk(2)实现,该系统调用扩充(或缩小)进程的堆。
在动态分配的缓冲区前或后进行写操作,破坏的可能不仅仅是该区的管理记录信息,因为在动态分配的缓冲区前或后可能用于其他动态分配的对象。
例:
#include <stdlib.h>
int main(){
char *p = malloc(5); //为p分配5字节的内存空间
p = "123"; //使p指向字符串123的首地址
free(p); //这里free会报错
//在malloc(5)执行后的确为p分配了一个为5的内存空间,记此时p的地址为a,但是p="123"之后p指向了"123"的首地址,记这个地址为b。要知道a是不等于b的,而且b不是动态分配的内存空间,因此无法通过free释放掉。
//关键在于p="123",并没有在分配给p的5个字节内存空间上赋值
//对比
int main(){
char *p = malloc(2);
p[0] = 'a';
p[1] = 'b';
}