7.1 概述
我们学习的编程语言都有一个“入口”,通常来说,我们会认为main函数即为程序的开始,其实不是这样的,在调用main函数之前,需要进行一系列的准备,比如准备进程所需的环境变量,外部传入的命令行参数等。
同样的,在main函数return时,通常意味着我们编写的程序结束了,但在程序结束之前通常也会做一些“善后”的工作,而在这些工作完成后,我们的程序才算是真正意义上的运行完毕了。
本章就带你对进程的环境一探究竟,带你了解进程环境那些事儿。
本章重点
程序的生命周期 程序的存储空间布局 共享库 内存分配 非局部跳转
7.2 程序的生命周期
一个标准C程序的main函数签名如下:
int main(int argc, char *argv[]);
当内核执行一个C程序时,在调用main函数之前会先调用一个特殊的“启动例程(start-up routine)”,可执行程序文件将这个启动例程指定为程序的起始地址。启动例程从内核取得命令行参数和环境变量,然后为main函数做好安排。
而在我们企图将进程结束时,通常会使用以下几种方式:
1. 在main函数内return
2. 调用exit
3. 调用_exit或_Exit
其中1和2比较常用,我们在初学C语言时,老师和书上都会告诉我们这两种方法。
1和2可以视为是等价的。相对于3来说,使用return和exit会进行一些额外的操作而不是立即结束程序,当我们在main函数内return或调用exit后,会再调用我们使用atexit函数注册的终止处理程序,然后再调用标准I/O清理程序(对于所有打开的流调用fclose函数,使所有缓冲中的数据都被flush),在进行这一系列清理关闭操作之后,再调用_exit或_Exit,此时程序进入内核态,即将“真正的终止”。
可以将exit函数看做是_exit或_Exit的包装函数,因为通过上文可以知道,exit函数在进行清理操作之后,最终还是会调用_exit或_Exit,_exit或_Exit立即进入内核态,不进行其他的操作。
7.3 存储空间布局
一个程序在硬盘上时,仅有.text段和.data段,当在运行该程序时,操作系统才会为其分配运行所必须的资源和结构(比如运行时堆和栈等)。.text段存放程序的机器指令,.data段存放已经初始化的数据。下面是一个运行中的程序a.out在内存中的布局(memory layout)。
7.4 共享库
共享库可以显著减少程序的大小,但会增加一些运行时开销,多个程序之间通过动态链接的方式共享副本。
假设有以下C代码:
#include <stdio.h>
int main(void) {
printf("hello, world");
return 0;
}
如果不使用共享库技术,那么我们编译的每个可执行文件里都将包含一份stdio的副本。而有了共享库技术,我们只需在内存中存放一份stdio的只读拷贝,让多个程序共享,这样就可以大大节省可执行文件的长度。
7.5 内存分配
内存这个东西有种很奇怪的属性“无论内存再大,总是会觉得不够用”。那么平时我们程序的内存都是怎样分配的呢?
我们来看看下面这段C程序:
void foo() {
int num = 5;
}
void bar() {
int *arr = malloc(sizeof(int) * 10);
free(arr);
}
foo函数内部定义了一个变量num,它的值为5,那么num这个变量就是分配在用户栈上的(图7.3-1中的栈),当foo函数的调用栈返回时,num会随其一起被销毁。(在底层[1]表现为%rsp寄存器增加,栈顶向高地址收缩,原本foo函数和变量num所在的栈帧此时就没有意义了)
而bar函数中有一个名叫arr的变量,该变量是一个数组指针,在bar中我们调用了malloc函数,该函数会在堆(heap)上申请一块内存,并将指向该内存的指针赋值给arr。
bar中的free函数释放malloc函数申请的内存,如果忘记了free或者将数组指针arr指向了别的内存位置,就会造成内存泄露(Memory Leak),泄露的内存无法被回收,久而久之会占用大量的内存,导致频繁的缺页中断,甚至可能会导致系统宕机或程序崩溃。
7.6 非局部跳转
你可能会听说过这样一句话“不要在你的程序内使用goto”。的确,大量的goto会导致程序控制流复杂,难以维护,但其实现在我们所见到的goto是已经被封印了的goto。
C、Java、Go等现代编程语言中的goto都只能在函数内部跳转,但有些编程语言里goto可要强大的多
这种语言的每行代码都有行号,你只需goto 行号就可以在代码中随意跳跃;甚至于,它的子程序也不过是行号不同的一段代码而已,你完全可以goto进某个子程序的倒数第三行、然后检测到它没有设置某个标记所以不能return于是goto回你认为的、主程序中的调用点!
————知乎用户invalid s[2]
C中的goto虽然只能在函数内跳转,但可以使用setjmp和longjmp函数来破除封印。需要注意的是,使用setjmp和longjmp函数时,要注意带来的问题,比如自动变量和寄存器变量的状态[3]等。
笔者个人认为,如果条件允许,还是尽可能不要使用goto或此类函数。
后记
这章花了挺久的时间,昨天还专门为此写了一篇文章:《久等了》。即便用了这么久,依然觉得这部分掌握的知识很少,之前粗读的CSAPP部分章节需要重新研读一下(这两本书结合起来,相辅相成挺舒服的)
另外我把我读过的书籍和心得体会都放出来了,在公众号界面点击底部菜单“书架 -> 读过的书”中就能看到啦。
参考资料
运行时栈: 《深入理解计算机系统》第三章-程序的机器级表示3.7节详细描述了程序运行的过程中,用户栈是如何变化的。
[2]basic里的goto: 这段选自知乎用户invalid s的回答,原链接https://www.zhihu.com/question/20259336/answer/1779133478
[3]longjmp后系统的状态: APUE7.10章节详细阐述了setjmp和longjmp的要点
我是赵不贪,喜欢写点儿有的没的,长按图片扫描二维码或微信搜索“阿贪爱学习”关注我的最新动态
原文始发于微信公众号(梦真日记):《APUE》 – Chapter7 进程环境
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/167892.html