1、引言
2、main函数
- main函数的原型
/*argc是命令行参数的数目,argv是指向各个指针所构成的数组*/ int main(int argc,char *argv[]);
3、进程终止
-
有八种方式使进程终止。其中5种是正常,它们是:
- 从main函数返回
- 调用exit
- 调用_exit或_Exit
- 最后一个线程从其启动例程返回
- 从最后一个线程调用pthread_exit
-
异常终止有三种方式:
- 调用abort
- 收到一个信号
- 最后一个线程对取消请求做出响应
3.1、退出函数
-
以下三个函数用于正常终止一个程序
void _exit(int status); //系统调用,立即进入内核 void _Exit(int status); //系统调用,立即进入内核 void exit(int status); //先执行一些清理工作,然后返回内核。
- exit函数总是执行标准I/O库的清理关闭操作,对于所有打开的流调用
fclose
函数,这会造成输出缓冲中的所有数据被写(冲洗)到文件上。 - 这三个退出函数都有一个参数,即终止状态(或退出状态)。
main
函数返回一个整形值与用该值调用exit
是等价的。于是在main
函数中exit(0);
等同于return 0;
- 当调用这些函数时不带终止状态,或者
main
执行了一个无返回值的return
语句,或者main
没有声明返回类型为整形,则进程的终止状态是未定义的。
- exit函数总是执行标准I/O库的清理关闭操作,对于所有打开的流调用
-
实例:
main
执行了一个无返回值的return
语句/*hello1.c*/ #include <stdio.h> int main() { printf("hello, world\n"); return;//无返回值 }
命令行:
lh@LH_LINUX:~/桌面/apue.3e/environ$ gcc -o hello hello1.c hello1.c: In function ‘main’: hello1.c:6:2: warning: ‘return’ with no value, in function returning non-void return; ^ lh@LH_LINUX:~/桌面/apue.3e/environ$ ./hello hello, world lh@LH_LINUX:~/桌面/apue.3e/environ$ echo $? 13
对程序进程编译然后运行,可见到其终止码是随机的。注意:
$?
指明上一次执行命令的返回值,同时回忆一下$#
、$*
、$?
、$0
、$1
…的作用
3.2、atexit函数
- 一个进程可以登记最多32个函数(一些操作系统实现可能更多)个函数,这些函数将由
exit
自动调用。我们称这些函数为终止处理程序,通过atexit
函数来登记这些函数int atexit(void (*function)(void));
- exit调用这些函数的顺序与登记它们的顺序相反,同一函数如果被登记多次也会被调用多次。
- exit函数会先调用各终止处理程序,再
fclose
所有打开流。然后再调用_exit
函数终止进程。 - 如果程序调用exec函数族,则会清除所有已经注册的终止处理程序。
- 内核使程序执行的唯一方法是调用一个
exec
函数。进程自愿终止的唯一方法是 显式或隐式(通过exit) 调用_exit
或_Exit
。进程也可以非自愿的由一个信号终止。 - 实例:使用
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_exit1登记了两次,则也会调用两次*/ 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);//等价于exit(0); } static void my_exit1(void) { printf("first exit handler\n"); } static void my_exit2(void) { printf("second exit handler\n"); }
可以看到root@LH_LINUX:/home/lh/桌面/apue.3e/environ# ./doatexit main is done first exit handler first exit handler second exit handler
exit
调用这些函数的顺序与登记它们的顺序相反
4、命令行参数
-
调用
exec
函数的进程可以将命令行参数传递给新程序。UNIX内核并不查看这些字符串,它们的解释完全取决于各个应用程序,因此需要通过exec
将这些参数传递给进程 -
实例:将所有命令行参数都回显到标准输出上。
#include "apue.h" int main(int argc, char *argv[]) { int i; for (i = 0; i < argc; i++) /* echo all command-line args */ printf("argv[%d]: %s\n", i, argv[i]); exit(0); }
命令行:
lh@LH_LINUX:~/桌面/apue.3e/environ$ ./echoarg arg1 arg2 arg3 argv[0]: ./echoarg argv[1]: arg1 argv[2]: arg2 argv[3]: arg3 lh@LH_LINUX:~/桌面/apue.3e/environ$ echo arg1 arg2 arg3 arg1 arg2 arg3
可以看到可执行文件
echoarg
将所有命令行参数都打印了出来,而echo
程序不会回显第0个参数。注意:argv[argc]
是一个空指针。
5、环境表
- 每一个进程都有一张环境表,该表也是一个字符指针数组,其中每个指针指向一个以
null
结束的C字符串地址。全局变量environ
包含了该指针数组的地址。extern char **environ;
- 每一个环境变量由
name=value
形式的字符串构成,其中name
字段一般是大写字母组成。 - 当然也可以通过main函数的第三个参数来访问环境变量(已弃用)
int main(int argc, char* argv[], char**envp);//envp已弃用,规定应使用全局变量environ
- 每一个环境变量由
6、C程序的存储空间布局
-
C程序由以下部分组成
- 正文段(或代码段).text:
由CPU执行的机器指令部分组成(即程序编译之后,编译器会将代码翻译成二进制的机器码,机器码存储在代码段(.text)中)。通常正文段可以共享,所以即使是频繁执行的程序在存储器中也只需有一个副本。并且正文段通常是只读的,以防止程序由于意外而修改其指令。也有可能包含一些只读的常数变量,例如字符串常量等。 - 初始化数据段.data:
通常将此段称为数据段。用于保存有非0初始值的全局变量和静态变量。(局部变量保存在栈中) - 未初始化数据段.bss:
用于保存没有初始值或初值为0的全局变量和静态变量。在程序开始执行之前,内核将此段中的数据初始化为0和空指针。 - 栈stack:
局部变量与每次函数调用时需要保存的信息存放在stack中。每次函数调用时,其返回地址以及调用者的环境信息(如某些寄存器的值)都存放入栈。然后,最近被调用的函数在栈上为其分配栈帧。递归的原理就是每次调用自身时,就用一个新的栈帧,因此一次函数调用实例中的变量不会影响到另一次函数调用实例中的变量。 - 堆heap:
通常在堆中进行动态存储分配(malloc)。位于bss和stack中间。
- 正文段(或代码段).text:
-
可执行文件中还有一些其他类型的段:包含符号表的段;包含调试信息的段;包含动态库链接表的段等。这些部分并不装载到进程执行的程序映像中。
-
可以看出,未初始化数据段的内容并不存放在磁盘程序文件中。因为内核在程序开始运行前将它们都设置为0。需要存放在磁盘程序文件中的只有正文段和初始化数据段。
7、共享库
- 共享库使得可执行文件中不再需要包含公用的库函数,而只需在所有进程都可引用的存储区中保存这种库例程的一个副本。
- 程序第一次执行或者第一次调用某个库函数时,用动态链接方法将程序与共享库函数相链接。这减少了每个可执行文件的长度,但增加了一些运行时间开销。这种时间开销发生在该程序第一次被执行时,或者每个共享库函数第一次被调用时。共享库的另一个优点是可以用库函数的新版本代替老版本而无需对使用该库的程序重新编译(假如参数个数与类型都不变)。
- 下面展示无共享库方式和使用共享库方式创建可执行文件。
可以发现:使用共享库比不使用时,可执行文件的正文和数据段的长度都显著减少。注意:lh@LH_LINUX:~/桌面/apue.3e/environ$ gcc -static hello1.c hello1.c:3:1: warning: return type defaults to ‘int’ [-Wimplicit-int] main() ^ lh@LH_LINUX:~/桌面/apue.3e/environ$ ls -l a.out -rwxrwxr-x 1 lh lh 912728 7月 7 20:54 a.out lh@LH_LINUX:~/桌面/apue.3e/environ$ size a.out text data bss dec hex filename 824102 7284 6360 837746 cc872 a.out lh@LH_LINUX:~/桌面/apue.3e/environ$ gcc hello1.c hello1.c:3:1: warning: return type defaults to ‘int’ [-Wimplicit-int] main() ^ lh@LH_LINUX:~/桌面/apue.3e/environ$ ls -l a.out -rwxrwxr-x 1 lh lh 8608 7月 7 20:54 a.out lh@LH_LINUX:~/桌面/apue.3e/environ$ size a.out text data bss dec hex filename 1183 552 8 1743 6cf a.out
size()
报告正文段、数据段和bss段的长度(以字节文单位),结果的第4列和第5列分别以十进制和十六进制表示的3段总长度。
8、存储空间分配
-
以下三个函数用于存储空间动态分配(在堆上分配)
void *malloc(size_t size); //分配指定字节数的存储区,此存储区中的初始值不确定 void *calloc(size_t nmemb, size_t size); //为指定数量指定长度的对象分配存储空间。该空间中的每一位都初始化为0 void *realloc(void *ptr, size_t size); //增加或减少以前分配区的长度。当增加长度时,可能需要将以前分配区的内容移到另一个足够大的区域,以便在尾端提供增加的存储区,而新增区的初始值不确定。 void free(void *ptr); //释放ptr指向的存储空间,被释放的空间通常被送入可用存储区池。之后,可在调用上述3个分配函数时再分配这些空间。
-
realloc函数使我们可以增减以前分配的存储区长度。比如我们在堆上有一个数组,想要扩充该数组的长度,并且在该存储区后有足够的空间可供扩充,则可以在原存储区位置上向高地址方向扩充,无需移动原先数组任何内容。如果在原存储区后没有足够空间,则realloc分配另一个足够大的存储区,将现有数组内容全部复制到新分配的存储区,然后释放原存储区,返回新存储区地址。如果ptr是NULL,则realloc与malloc函数功能相同。
-
这些分配函数通常底层使用sbrk系统调用。该系统调用扩充或缩小进程的堆。
-
虽然sbrk可以缩小堆区大小,但是大多数malloc和free的实现都不减少进程的存储空间,释放的空间可供以后再分配,将它们保存在malloc池中而不返回给内核
-
大多数实现所分配的存储空间比所要求的要稍微大一些,额外的空间用来记录管理信息:分配块的长度、指向下一个分配块的指针等。这意味着如果超过一个已分配区的尾端或者在已分配区起始位置之前进行写操作,则会改写另一块的管理记录信息或其他动态分配对象,这种错误是灾难性的。
-
致命错误:释放了一个已经释放了的块;调用free时使用的指针不是3个alloc函数的返回值等。
-
若使用malloc函数在堆上动态分配内存空间但是忘记调用free函数,那么该进程占用的存储空间就会连续增加,这称为内存泄漏。如果不调用free释放不再使用的空间,那么进程地址空间长度会慢慢增加,直至不再有空闲空间。
-
-
在栈上分配内存空间
void *alloca(size_t size);
- 它的调用方式与
malloc
相同,但是在当前函数的栈帧上分配存储空间而不是在堆中。 - 优点:当函数返回时自动释放它所使用的栈帧,不用手动free释放
- 缺点:增加了栈帧的长度,而某些系统的函数在已经被调用后不能增加栈帧长度,于是也不支持alloca函数。
- 它的调用方式与