原文地址http://www.freecls.com/a/2712/2b
进程是一个可执行程序的实例。程序包含了一系列信息,这些信息描述了如何在运行时创建一个进程,所包含的内容如下。
1.二进制格式标识,linux上用的是elf格式
2.机器语言指令
3.程序入口地址
4.数据
5.符号表和重定位表
6.共享库和动态链接信息
7.其他信息
一个程序可以创建出很多进程,换句话说一个可执行文件可以执行多次,比如nginx服务器程序可以执行多个,让多个nginx进程同时在后台运行。
从内核的角度看,进程是由用户内存空间和一些列内核数据结构组成,其中用户内存空间包含了程序的代码及使用的变量,而内核数据结构用来维护进程的状态信息,包括与进程相关的标识号、虚拟内存表、打开文件描述表、信号传递及处理相关信息、进程资源使用限制、当前的工作目录和其他信息。
每个进程都一个进程号,每个进程又有父进程,所以会成为树状结构,当父进程终止时,子进程会变成孤儿进程,就会被进程号为1的进程收养(centos7为systemd)。
linux的/proc/PID/status文件记录了进程的详细信息比如ppid
#include
pid_t getpid(void); //获取进程号
pid_t getppid(void); //获取父进程号
进程在内存中的布局
1.文本段包含了进程的机器指令,为了防止进程指针错误意外的修改了自身指令,所以是只读的。多个进程可以同时运行同一程序,它们的机器指令又是一样的,所以文本段是可共享的。
2.初始化数据段包含了显示初始化的全局变量和静态变量,这些数据已经在磁盘的可执行文件里,所以当程序加载到内存时,就可以从磁盘的程序文件中读取这些变量值。所以初始化的数据越多,可执行文件越大。
3.未初始化数据段包含了未进行显示初始化的全局变量和静态变量。程序启动之前,系统将本段内所有内存初始化为0.由于历史的原因,此段常被称为BSS段。与初始化数据段不同的是,它几乎不占用可执行文件大小,可执行文件中只记录未初始化数据段的大小及位置,直到运行时再由程序加载器来分配这一空间。
4.栈(stack)是一个动态增长和收缩的段,由栈帧组成。系统会为每个当前调用的函数分配一个栈帧。栈帧中存储了函数的局部变量(自动变量)、实参和返回值。
5.堆是在运行时动态内存分配的一块区域。比如调用(malloc,calloc)。
使用size命令可现实二进制可执行文件的文本段(text)、初始化数据段(data)、非初始化数据段(bss)的大小。
虚拟内存管理
linux,像多数现代内核一样,采用了虚拟内存管理技术。虚拟内存的规划之一是将每个程序使用的内存切割成小型的、大小固定的页单元。相应的,将ram划分成一系列与虚拟页尺寸相同的页帧。任一时刻,每个程序仅有部分页需要驻留在屋里内存页帧中。这些页构成了所谓的驻留集(resident set)。程序未使用的页可以拷贝保存到交换空间(swap)--这是磁盘空间的保留区,仅在需要的时候才会载入物理内存。
为了支持这个特性,内核需要为每个进程维护一张页表来映射虚拟页实际的内存地址或者是在交换空间。
由于内核能够为进程分配和释放页,所以虚拟拟地址范围可以发生变化
1.栈向下增长超出了之前达到的位置。
2.堆中分配和释放内存时改变了堆顶(program break)位置。
3.当调用shmat连接共享内存或shmdt脱离共享内存时。
4.当调用mmap创建内存映射或munmap解除内存映射时。
虚拟内存的优点
1.进程与进程、进程与内核相互隔离,防止一个进程读取和修改另一个进程的内存,因为每个进程的虚拟内存对应的实际物理内存都不一样。
2.如果需要,可以让不同进程共享内存,因为可以让不同进程的页表指向相同的物理内存。
3.可以实现内存保护机制,也就是说可以对页表进行标记来表示可读、可写、可执行。多个进程共享的物理内存时,一个进程只读,另一个进程可以读写。
4.程序员、编译器、连接器之类的工具无需关注程序在RAM中的物理布局。
5.因为需要驻留在内存中的仅是程序的一部分,所以程序加载和运行都很快。而且一个进程所占用的内存(即虚拟内存)能够超出RAM的容量。
6.由于每个进程使用的物理内存减少了,所以内存中进程数量变多了,所以cpu可执行的程序变多了,旺旺提高了cpu的利用率。
命令行参数arc,argv
每个c语言程序都必须有一个成为main()的函数作为程序启动的起点,通过2个入参int argc(表示参数个数),char *argv[](是一个指向命令行参数的指针数组)。内存存放如下图
环境列表
每一个进程都有与其相关的环境列表的字符串数组。每个字符串都以name=value形式定义,可存储任何信息,常将列表中的名称成为环境变量。
新进程创建时,会继承父进程的环境列表,所以可以拿来做简单的单项进程通信。
想在shell中添加环境变量可执行
SHELL=/bin/bash
export SHELL
#这样在a.out执行后,里面就会有SHELL这个环境变量
./a.out
linux下的/proc/PID/environ文件记录了进程的环境列表。
在C语言中可以使用char **environ全局变量来访问环境列表。当然也可以在main函数中声明第三个参数来访问环境列表int main(int argc, char **argv, char **envp)。内存数据结构如下图
#include
#include
int main(int argc, char **argv, char **env){
printf("%p %p %p\n", &argc, argv, env);
printf("arg[0]为:%s\n", argv[0]);
if(argv[1] == NULL) printf("argv[1]为一个空指针NULL\n");
printf("argv[2]的地址:%p\n", &argv[2]);
printf("环境变量的开始地址:%p\n\n\n", &env[0]);
int i = 0;
while(env[i] != NULL){
printf("%s\n", env[i]);
i++;
}
}
/*
./a.out
不带任何参数运行
*/
/*
0x7ffd3f4694ec 0x7ffd3f4695e8 0x7ffd3f4695f8
第一个参数为:./a.out
argv[1]为一个空指针NULL
argv[2]的地址:0x7ffd3f4695f8
环境变量的开始地址:0x7ffd3f4695f8
XDG_SESSION_ID=13545
HOSTNAME=izj6cfw9yi1iqoik31tqbgz
TERM=xterm
SHELL=/bin/bash
HISTSIZE=1000
...
...
*/
从上述代码可以看出,命令行参数字符串数组和环境列表字符串数组在内存里由一个空指针NULL连接着。
char *getenv(const char *name);
获取环境变量字符串,为了可移植性,尽量别修改该函数返回的字符串,有可能是坏境的一部分
int putenv(char *string);
参数string是一个指针,指向name=value形式的字符串。调用成功后,environ变量中的某个元素将指向string,随后修改string将影响进程的坏境。所以string参数不应该为自动变量,因为定义此变量的函数一旦返回,该变量就会被释放。失败返回0。
int setenv(const char *name, const char *value, int overwrite);
这个函数以参数名,参数值的形式传递,并会分配一块内存缓冲区,将name和value复制到里面。overwrite大于0代表覆盖。
int unsetenv(const char *name);
移除名为name坏境变量。
int clearenv(void);
清除环境变量,等同于environ = NULL,注意内存泄漏,因为setenv会分配内存,而clearenv只是简单的把environ置为NULL。
#include
#include
#include
extern char **environ;
int main(int argc, char **argv){
int i = 0;
printf("before putenv and setenv:%p\n", environ);
char s[20] = "url=freecls";
putenv(s);
setenv("name", "freecls", 1);
//从地址改变可以看出,为了能容纳更多环境变量,environ这个指针数组重新分配了内存。
printf("after putenv and setenv:%p\n\n", environ);
while(environ[i] != NULL){
printf("%p %s\n", &environ[i], environ[i]);
i++;
}
//在这里又设置一个环境变量
setenv("name1", "freecls1", 1);
puts("\n\n");
//发现environ的地址又变了,表明environ又重新分配了内存
//但是里面的原先字符串地址没变,只是重新分配了数组的大小而已
i=0;
while(environ[i] != NULL){
printf("%p %s\n", &environ[i], environ[i]);
i++;
}
printf("\n\n------------------------\n\n");
i=0;
while(environ[i] != NULL){
printf("%p %s\n", &environ[i][0], environ[i]);
i++;
}
}
//内存地址长的代表栈内存,内存地址短的代表堆内存(动态分配的内存)
/*
before putenv and setenv:0x7ffd77eadae8
after putenv and setenv:0x2009010
0x2009010 XDG_SESSION_ID=13703
0x2009018 HOSTNAME=izj6cfw9yi1iqoik31tqbgz
0x2009020 TERM=xterm
0x2009028 SHELL=/bin/bash
...
0x20090b8 _=./a.out
0x20090c0 url=freecls
0x20090c8 name=freecls
0x2009130 XDG_SESSION_ID=13703
0x2009138 HOSTNAME=izj6cfw9yi1iqoik31tqbgz
0x2009140 TERM=xterm
0x2009148 SHELL=/bin/bash
...
0x20091d8 _=./a.out
0x20091e0 url=freecls
0x20091e8 name=freecls
0x20091f0 name1=freecls1
------------------------
0x7fffc8e3c87c XDG_SESSION_ID=13703
0x7fffc8e3c891 HOSTNAME=izj6cfw9yi1iqoik31tqbgz
0x7fffc8e3c8b2 TERM=xterm
0x7fffc8e3c8bd SHELL=/bin/bash
...
0x7fffc8e3cfe6 _=./a.out
0x7fffc8e3b520 url=freecls
0x1aa70e0 name=freecls
0x1aa7010 name1=freecls1
*/
通过上述例子可以看出,为了让environ字符串数组能够容纳新增加的环境变量,每次新增一个环境变量时,environ都得重新分配内存。
非局部跳转:setjmp(),longjmp()
如果是函数内部有跳转需求可以使用goto,setjmp()和longjmp是用来执行函数间跳转的。
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
setjmp()调用为后续的longjmp()调用确立跳转目标。调用setjmp()时,env除了存储当前进程的其他信息外,还保存了程序计数寄存器(指向当前正在执行的机器指令)和栈指针寄存器(标记栈顶)的副本。这些数据可以让longjmp()完成2个重要的步骤。
1.将发起longjmp()和之前调用setjmp()函数之间的函数栈帧从栈上剥离。这是通过将栈指针寄存器重置为env参数内保存的值来实现的。
2.重置程序计数寄存器,使程序得以从setjmp()调用的位置继续执行。同样,此功能是通过env参数中保存的值(程序计数寄存器)来实现的。
#include
#include
static jmp_buf env;
static void f2(void){
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: /* This is the return after the initial setjmp() */
printf("Calling f1() after initial setjmp()\n");
f1(argc); /* Never returns... */
break; /* ... but this is good form */
case 1:
printf("We jumped back from f1()\n");
break;
case 2:
printf("We jumped back from f2()\n");
break;
}
return 0;
}
/*
./a.out
Calling f1() after initial setjmp()
We jumped back from f1()
./a.out a
Calling f1() after initial setjmp()
We jumped back from f2()
*/
优化编译器的问题
优化编译器会重组指令执行顺序,并把变量从内存读取到寄存器中改变时,不立马放回内存。我们看下面的例子。
#include
#include
#include
static jmp_buf env;
static void doJump(int nvar, int rvar, int vvar){
printf("Inside doJump(): nvar=%d rvar=%d vvar=%d\n", nvar, rvar, vvar);
longjmp(env, 1);
}
int main(int argc, char *argv[]){
int nvar;
register int rvar; /* Allocated in register if possible */
volatile int vvar; /* See text */
nvar = 111;
rvar = 222;
vvar = 333;
if (setjmp(env) == 0) { /* Code executed after setjmp() */
nvar = 777;
rvar = 888;
vvar = 999;
doJump(nvar, rvar, vvar);
} else { /* Code executed after longjmp() */
printf("After longjmp(): nvar=%d rvar=%d vvar=%d\n", nvar, rvar, vvar);
}
return 0;
}
/*
gcc main.c
./a.out
Inside doJump(): nvar=777 rvar=888 vvar=999
After longjmp(): nvar=777 rvar=888 vvar=999
gcc main.c -O
./a.out
Inside doJump(): nvar=777 rvar=888 vvar=999
After longjmp(): nvar=111 rvar=222 vvar=999
*/
经过优化过的程序,nvar和rvar在赋值时
nvar = 777;
rvar = 888;
会先把777,888暂时放在寄存器里而不会写入内存,所以当下面调用longjmp()复原时,该寄存器里存放的777,888丢失,所以内存里独到的还是之前的初始值。所以如果不希望编译器优化某些变量,可以在声明的时候加上volatile限定符。
最后提一下,能不用goto、setjmp、longjmp就不用,因为这些会加大代码的阅读负担,所以在程序设计之初就应该避免。
本文对linux进程以及相关的做了简单的介绍,如果有疑问可以给我留言。
1.编译器版本gcc4.8,运行环境centos7 64位
2.原文地址http://www.freecls.com/a/2712/2b