进程的概念
进程是一个可执行程序的实例。而程序包含了一系列的文件信息,这些信息描述了如何在运行时创建一个进程,其所包括的内容有:
- 二进制格式标识:用于描述可执行文件格式的元信息,内核用它来解释文件中的其他信息。Linux采用
ELF文件系统
。 - 机器语言指令
- 程序入口地址
- 数据:程序文件包含的变量初始值和程序使用的字面常量值(如字符串).
- 符号表及重定位表
- 共享库和动态链接库的信息
- 其他信息,用以描述如何创建进程。
可以用一个程序创建许多个进程,反过来,许多进程运行的可以是同一个程序。从内核角度来说,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码以及代码所使用的变量,而内核数据结构则用于维护进程状态信息,如进程号、虚拟内存表、进程打开的文件描述符、信号传递及处理的有关信息、当前工作目录、进程资源使用及限制和其他大量信息。
一个进程运行时的信息可以在/proc/{PID}/
目录下看到。
进程内存布局
每个进程所分配的内存由许多部分组成,通常称之为段
,各个段如下:
- 文本段:包含了进程运行的程序机器语言指令。文本段具有只读属性,以防止进程通过错误指针意外修改自身指令;同时,文本段是共享的,从而可以让一份程序代码的拷贝映射到所有共享这份代码的进程中,从而让多个进程运行同一个程序。
- 初始化数据段:包含显示初始化的全局变量和静态变量。
- 未初始化的数据段:包含了为进程显示初始化的全局变量和静态变量,程序启动之前,系统将本段内所有内存初始化为0,该段常被成为BSS段。为初始化和初始化的变量分开存放主要是由于没有必要为未初始化的变量在文件中分配存储空间,相反,可执行文件只要记录未初始化数据段的位置及其所需要的大小,知道运行时再由程序加载器来分配空间。
- 栈是一个动态增长和收缩的段,由
栈帧
组成。系统为每个当前调用的函数分配一个栈帧。栈帧中存储了函数的局部变量(即自动变量)、实参和返回值。 - 堆是可在运行时动态进行内存分配的一块区域。堆顶称为
program break
。
大多数UNIX实现(包括Linux)中C语言编程环境提供了3个全局变量etext
、edata
、end
,它们分别用来标识文本段
、初始化数据段
、非初始化数据段
结尾处的下一个字节位置。在x86-32体系结构进程在内存中的布局如下所示:
1 | 虚拟内存地址(从下往上增长) |---------------------------------------- | Kernel映射到进程虚拟内存,区域 | 提供了内存符号的地址。(/proc/kallsyms) (无法使用) |---------------------------------------- | argv, environ |----------------------------------------栈顶 | 栈(向下增长) |---------------------------------------- | | 未分配的内存(无法使用) | |----------------------------------------程序中断 | 堆(向上增长) |----------------------------------------end | 未初始化的数据(bss) |----------------------------------------edata | 初始化的数据 |----------------------------------------etext | 文本段 |-----------------------------------------0x08048000 | (无法使用) |-----------------------------------------0x00000000 |
上述布局存在于虚拟内存之中。
命令行参数和环境变量
上一节的内存布局图中,argv和eviron
那一部分存储了进程启动时用户输入的命令行参数和环境变量,关于命令行参数,即main
函数的参数。
1 | #include <stdio.h> |
关于环境变量,每一个进程都有与之相关联的环境变量列表,其结构式字符串数组,每一个字符串以name=value
形式定义。新进程在创建之时,会继承其父进程的环境变量副本,这是一种原始的进程间通信方式,却颇为有用。
关于环境变量的几个接口和变量如下:
1 | #include <stdlib.h> |
需要注意的是,putenv
不为字符串分配一个缓冲区,它仅仅改变对应的environ
数组中的对应指针,因此string
参数应该是一个静态或者全局变量,而不是自动变量。
setenv
会为字符串分配一个内存缓冲区,并将name和value的字符串复制到该缓冲区中,以此来创建一个新的环境变量。若overwrite
值非0,那么setenv
总是覆写原来的环境变量;否则,若环境变量已经存在,那么它不会改变。
clearenv
仅仅是把environ
置为NULL,在某些时候可能会导致内存泄漏。
一个例子:
1 |
|
1 | ubuntu@VM-121-120-ubuntu:~/UNIX-Demos/proc_demos$ ./a.out G="HAGA" G=HAGA GREET=Hello world ubuntu@VM-121-120-ubuntu:~/UNIX-Demos/proc_demos$ ./a.out G="HAGA" GREET="Hello" G=HAGA GREET=Hello |
非局部跳转
C语言中的goto
语句允许我们进行语句跳转,但是这仅仅局限在同一个函数中。虽然跳转会让我们的代码变得难以维护,看着就头大,但是有时候有这样一个语句还是能解决很大的问题的,前提是不要滥用。为了能够解决非局部跳转的问题,UNIX系统提供了两个接口:
1 | #include <setjmp.h> |
setjmp
为后续的longjmp
设立了跳转目标,setjmp
把当前进程环境的各种信息保存到env
参数中,调用longjmp
时必须指定相同的env
变量,一般将env
设定为全局变量。
一个例子
1 | #include <stdio.h> |
输出:
1 | ubuntu@VM-121-120-ubuntu:~/UNIX-Demos/proc_demos$ ./a.out Calling f1() after initial setjmp() We jumped back from f1() ubuntu@VM-121-120-ubuntu:~/UNIX-Demos/proc_demos$ ./a.out ss Calling f1() after initial setjmp() We jumped back from f2() |