Zach的博客

进程

进程的概念

进程是一个可执行程序的实例。而程序包含了一系列的文件信息,这些信息描述了如何在运行时创建一个进程,其所包括的内容有:

  • 二进制格式标识:用于描述可执行文件格式的元信息,内核用它来解释文件中的其他信息。Linux采用ELF文件系统
  • 机器语言指令
  • 程序入口地址
  • 数据:程序文件包含的变量初始值和程序使用的字面常量值(如字符串).
  • 符号表及重定位表
  • 共享库和动态链接库的信息
  • 其他信息,用以描述如何创建进程。

可以用一个程序创建许多个进程,反过来,许多进程运行的可以是同一个程序。从内核角度来说,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码以及代码所使用的变量,而内核数据结构则用于维护进程状态信息,如进程号、虚拟内存表、进程打开的文件描述符、信号传递及处理的有关信息、当前工作目录、进程资源使用及限制和其他大量信息。

一个进程运行时的信息可以在/proc/{PID}/目录下看到。

进程内存布局

每个进程所分配的内存由许多部分组成,通常称之为,各个段如下:

  • 文本段:包含了进程运行的程序机器语言指令。文本段具有只读属性,以防止进程通过错误指针意外修改自身指令;同时,文本段是共享的,从而可以让一份程序代码的拷贝映射到所有共享这份代码的进程中,从而让多个进程运行同一个程序。
  • 初始化数据段:包含显示初始化的全局变量和静态变量。
  • 未初始化的数据段:包含了为进程显示初始化的全局变量和静态变量,程序启动之前,系统将本段内所有内存初始化为0,该段常被成为BSS段。为初始化和初始化的变量分开存放主要是由于没有必要为未初始化的变量在文件中分配存储空间,相反,可执行文件只要记录未初始化数据段的位置及其所需要的大小,知道运行时再由程序加载器来分配空间。
  • 栈是一个动态增长和收缩的段,由栈帧组成。系统为每个当前调用的函数分配一个栈帧。栈帧中存储了函数的局部变量(即自动变量)、实参和返回值。
  • 堆是可在运行时动态进行内存分配的一块区域。堆顶称为program break

大多数UNIX实现(包括Linux)中C语言编程环境提供了3个全局变量etextedataend,它们分别用来标识文本段初始化数据段非初始化数据段结尾处的下一个字节位置。在x86-32体系结构进程在内存中的布局如下所示:

1
虚拟内存地址(从下往上增长)
          |----------------------------------------
          |	     Kernel映射到进程虚拟内存,区域          
          |      提供了内存符号的地址。(/proc/kallsyms) (无法使用)
          |----------------------------------------
          |		argv, environ			    
          |----------------------------------------栈顶
          |		栈(向下增长)
          |----------------------------------------
          |
          |		未分配的内存(无法使用)
          |
          |----------------------------------------程序中断
          |     堆(向上增长)
          |----------------------------------------end
          |     未初始化的数据(bss)
          |----------------------------------------edata
          |     初始化的数据
          |----------------------------------------etext
          |     文本段
          |-----------------------------------------0x08048000
          |     (无法使用)
          |-----------------------------------------0x00000000

上述布局存在于虚拟内存之中。

命令行参数和环境变量

上一节的内存布局图中,argv和eviron那一部分存储了进程启动时用户输入的命令行参数和环境变量,关于命令行参数,即main函数的参数。

1
2
3
4
5
6
7
8
#include <stdio.h>

int main(int argc, char **argc) {
int i;
for (i=0; i<argc; i++)
printf("argv[%d] = %s\n", i, argv[i]);
return 0;
}

关于环境变量,每一个进程都有与之相关联的环境变量列表,其结构式字符串数组,每一个字符串以name=value形式定义。新进程在创建之时,会继承其父进程的环境变量副本,这是一种原始的进程间通信方式,却颇为有用。

关于环境变量的几个接口和变量如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdlib.h>
extern char **environ; // 全局变量,用于访问进程的环境变量,每一个以`name=value`的形式

char* getenv(const char* name); // 成功返回环境变量字符串,否则返回NULL

int putenv(char *string); // 0表示公共,否则失败。`string`为`name=value`形式

int clearenv(); // 0表示成功,否则失败

// 注意以下函数不用`name=value`形式
int setenv(const char *name, const char *value, int overwrite); // 0表示成功,-1表示失败

int unsetenv(const char *name); // 0表示成功,-1表示失败。

需要注意的是,putenv不为字符串分配一个缓冲区,它仅仅改变对应的environ数组中的对应指针,因此string参数应该是一个静态或者全局变量,而不是自动变量。

setenv会为字符串分配一个内存缓冲区,并将name和value的字符串复制到该缓冲区中,以此来创建一个新的环境变量。若overwrite值非0,那么setenv总是覆写原来的环境变量;否则,若环境变量已经存在,那么它不会改变。

clearenv仅仅是把environ置为NULL,在某些时候可能会导致内存泄漏。

一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>

#define errExit(fmt, ...) do {fprintf(stderr, fmt, ##__VA_ARGS__); perror(""); exit(-1);} while (0)
extern char **environ;

int main(int argc, char **argv) {
char **ep;

clearenv();

for (int i=1; i<argc; i++) {
if (putenv(argv[i]) != 0)
errExit("putenv: %s", argv[i]);
}

if (setenv("GREET", "Hello world", 0) == -1)
errExit("setenv");

unsetenv("BYE");

for (ep=environ; *ep!=NULL; ep++)
puts(*ep);

return 0;
}
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
2
3
4
#include <setjmp.h>

int setjump(jump_buf env); // 0表示初始化操作,非0时返回的是`longjmp`设定的`val`值
void longjmp(jump_buf env, int val);

setjmp为后续的longjmp设立了跳转目标,setjmp把当前进程环境的各种信息保存到env参数中,调用longjmp时必须指定相同的env变量,一般将env设定为全局变量。

一个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <stdio.h>
#include <setjmp.h>

static jmp_buf env;

static void f2() {
longjmp(env, 2);
}

static void f1(int argc) {
if (argc == 1)
longjmp(env, 1);
f2();
}

int main(int argc, char **argv) {
switch (setjmp(env)) {
case 0: {
printf("Calling f1() after initial setjmp()\n");
f1(argc);
break;
}
case 1: {
printf("We jumped back from f1()\n");
break;
}
case 2: {
printf("We jumped back from f2()\n");
break;
}
default:
break;
}

return 0;
}

输出:

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()