进程和程序

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

    • 二进制格式标识
    • 机器语言指令
    • 程序入口地址
    • 数据
    • 符号表及重定位表
    • 共享库和动态链接信息
    • 其他信息
  • 可以用一个程序来创建许多进程,或者反过来说,许多程序运行的可以是同一程序
  • 进程是由内核定义的抽象的实体,并为该实体分配用以执行程序的各项系统资源
  • 从内核角度看,进程由用户内存空间和一系列内核数据结构组成,其中用户内存空间包含了程序代码及代码所使用的变量,而内核数据结构则用于维护进程状态信息。

进程号和父进程号

每个进程都有一个进程号(PID),进程号是一个正数,用以唯一标识系统中的某个进程。
系统调用getpid()返回调用进程的进程号

#include <unistd.h>
pid_t getpid(void);
                       // Always successfully returns process ID of caller
返回值
  • 成功:返回进程ID

每个进程都有一个创建自己的父进程。使用系统调用getppid()可以检索到父进程的进程号

#include <unistd.h>
pid_t getppid(void);
                       // Always successfully returns process ID of caller
返回值
  • 成功:返回进程ID

如果子进程的父进程终止,则子进程就会变成“孤儿”,init进程随即将收养该进程,子进程后续对getppid()的调用将返回进程号1

进程内存布局

每个进程所分配的内存由很多部分组成,通常称之为“段(segment)”。如下所示:

  • 文本段
  • 初始化数据段
  • 未初始化数据段(bss)
  • 栈(stack)
  • 堆(heap)
    size命令可以显示二进制可执行文件的文本段、初始化数据段、非初始化数据段(bss)的段大小

进程内存布局如下图所示:
Linux-process-memory-layout.png

虚拟内存管理

上述关于进程内存布局的这一布局存在于虚拟内存中。
虚拟内存管理技术,该技术利用了大多数程序的一个典型特征,即访问局部性,以求高效实用CPU和RAM(物理内存)资源。大多程序都展现了两种类型的局部性:

  • 空间局部性:是指程序倾向于访问在最近访问过的内存地址附近的内存
  • 时间局部性:是指程序倾向于在不久的将来再次访问最近刚访问过的内存地址

正是由于访问局部性特征,使得程序即便仅有部分地址空间存在于RAM中,依然可能得以执行。

虚拟内存的规划:

  1. 将每个程序使用的内存切割成小型的、固定大小的“页(page”单元
  2. 将RAM划分成一系列与虚拟页尺寸相同的页帧
  3. 任一时刻,每个程序仅有部分页需要驻留在物理内存页帧中。这些页构成了所谓驻留集(resident set
  4. 程序未使用的页拷贝保存在交换区(swap area内————这是磁盘空间中的保留区域,作为计算机RAM的补充。仅在需要时才会载入物理内存
  5. 若进程欲访问的页面目前并未驻留在物理内存中,将会发生页面错误(page fault),内核即刻挂起进程的执行,同时从磁盘中将该页面载入内存
  6. 为支持这一组织方式,内核需要为每个进程维护一张页表(page table。该页表描述了每页在进程虚拟地址空间中的位置。页表中的每个条目要么指出一个虚拟页面在RAM中的所在位置,要么表明其当前驻留在磁盘上

虚拟内存管理使进程的虚拟地址空间与RAM物理地址空间隔离开来,这带来许多优点:

  • 进程与进程、进程与内核相互隔离
  • 适当情况下,两个或者更多进程能够共享内存
  • 便于实现内存保护机制
  • 程序员和编译器、链接器之类的工具无需关注程序在RAM中的物理布局
  • 因为需要驻留在内存中的仅是程序的一部分,所以程序的加载和运行都很快

栈和栈帧

函数的调用和返回使栈的增长和收缩呈线性。栈驻留在内存的高端并向下增长(朝堆的方向)。专用寄存器————栈指针(stack pointer,用于跟踪当前栈顶。每次调用函数时,会在栈上新分配一帧,每当函数返回时,再从栈上将此帧移去。

有时会用用户栈(user stack来表示此处所讨论的栈,以便与内核栈区分开来。内核栈是每个进程保留在内核内存中的内存区域,在执行系统调用过程中供内部函数调用使用。

每个(用户)栈帧包括如下信息:

  • 函数实参和局部变量
  • (函数)调用的链接信息

因为函数能够嵌套调用,所以栈中可能有多个栈帧。

命令行参数(argcargv

每个C语言程序都必须有一个称为main()的函数,作为程序启动的起点。
命令行参数(由shell逐一解析)通过两个入参提供给main()函数。参数如下所示:

  • int argc:表示命令行参数的个数
  • char *argv[]:是一个指向命令行参数的指针数组,每一个参数又都是以空字符(null)结尾的字符串。argv[0]指向的通常是该程序的名称

argc/argv参数机制的局限之一在于这些变量仅对main()函数可用。要为其他函数所用,必须把参数进行传递或设置一个指向该参数的全局变量。

许多程序使用getopt()库函数解析命令行选项(即以“-”符号开头的参数)。

环境列表

每一个进程都有与其相关的称之为环境列表的字符串数组,或简称为环境。其中每个字符串都以 名称=值 形式定义。常将列表中的名称称为环境变量

新进程在创建之时,会继承其父进程的环境副本。这是一种原始的进程间通信方式,却颇为常用。

从程序中访问环境

  1. 可以使用全局变量char **environ(字符串数组)访问环境列表
  2. 可以通过声明main()函数中的第三个参数来访问环境列表:

    int main(int argc, char *argv[], char *envp[])
  3. getenv()函数能够从进程环境中检索单个值

    #include <stdlib.h>
    char *getenv(const char *name);
                         // Returns pointer to (value)string, or NULL if no such variable
    参数
    • name:环境变量名称
    返回值

    成功:返回环境变量值
    失败:返回NULL

修改环境

putenv()函数向调用进程的环境列表中添加一个新变量,或者修改一个已存在的变量值

#include <stdlib.h>
char *putenv(char *string);
                     // Returns 0 on success, or nonzero on error
参数
  • string:环境变量的“名称=值”对形式的字符串。该字符串不能为自动变量,因为环境列表会直接指向该字符地址而不是拷贝一个副本
返回值

成功:返回0
失败:返回非0

setenv()函数可以替代putenv()函数,向环境变量添加一个变量

#include <stdlib.h>
char *setenv(const char *name, const char *value, int overwrite);
                     // Returns 0 on success, or -1 on error
参数
  • name:环境变量的名称
  • value:环境变量的值
  • overwrite:如果存在,是否覆盖
返回值

成功:返回0
失败:返回-1

unsetenv()函数从环境中移除由name参数标识的变量

#include <stdlib.h>
char *unsetenv(const char *name);
                     // Returns 0 on success, or -1 on error
参数
  • name:环境变量的名称
返回值

成功:返回0
失败:返回-1

clearenv()函数用于清除整个环境

#include <stdlib.h>
int clearenv(void);
                     // Returns 0 on success, or nonzero on error
返回值

成功:返回0
失败:返回非0

执行非局部跳转:setjmp()longjmp()

使用库函数setjmp()longjmp()可执行非局部跳转。术语“非局部”是指跳转的目标为当前执行函数之外的某个位置。
C语言包含goto语句,但是存在一个限制————不能从当前函数跳转到另一函数。setjmp()longjmp()就弥补了这点。

#include <setjmp.h>
int setjmp(jmp_buf env);
                    // Returns 0 on initial call, nonzero on return via longjmp()
void longjmp(jmp_buf env, int val);

setjmp()调用为后续由longjmp()调用执行的跳转确立了跳转目标。该目标正是程序发起setjmp()调用的位置。从编程角度看来,调用longjmp()函数后,看起来就和从第二次调用setjmp()返回时完全一样。通过查看setjmp()返回的整数值可以区分是初始返回还是第二次返回。

参数
  • env:环境,setjmp()将当前进程环境的各种信息保存到env参数中,该变量应是全局变量
  • val:指定跳转后setjmp()返回的值,以此区分是否是初始返回。初始返回0
返回值(setjmp()

成功:初始化调用返回0,跳转后的二次返回返回非0

总结

本章的核心内容在进程布局,这涉及到虚拟内存、栈和栈帧等概念,其次介绍了进程的其他内容:参数和环境。

标签: Linux, C/C++, TLPI

添加新评论