LXR | KVM | PM | Time | Interrupt | Systems Performance | Bootup Optimization

《Linux/UNIX系统编程手册》第6章 进程

关键词:getpid()、getppid()、environ、setjmp()、longjmp()等等。

 

本章将研究进程结构,并重点关注进程虚拟内存的布局及内容。还会对进程某些属性进行考察。

1. 进程和程序

 进程是一个可执行程序的实例。

程序包含:

  • 二进制格式标识:每个程序文件都包含用于描述可执行文件格式的元信息。
  • 机器语言指令:对程序算法进行编码。
  • 程序入口地址:标识程序开始执行时的起始指令位置。
  • 数据:程序文件包含的变量初始值和程序使用的字面常量值。
  • 符号表及重定位表:描述程序中函数和变量的位置及名称。
  • 共享库和动态链接信息:程序文件所包含的一些字段,列出了陈旭运行时需要使用的共享库,以及加载共享库的动态链接器的路径名。
  • 其他信息:程序文件还包含许多其他信息,用以描述如何创建进程。

从内核角度看,进程是由用户内存空间和一系列内核数据结构组成,其中用户空间包含程序代码及代码所使用的变量,而内核数据结构则用于维护进程状态信息。

2. 进程号和父进程号

获取进程号:

#include <unistd.h>
pid_t getpid(void);
    Always successfully returns process ID of caller

 查看当前系统支持的最大信号数,通过/proc/sys/kernel/pid_max一般为32768。

int pid_max = PID_MAX_DEFAULT;

#define PID_MAX_DEFAULT (CONFIG_BASE_SMALL ? 0x1000 : 0x8000)

查看进程父进程号:

#include <unistd.h>
pid_t getppid(void);
    Always successfully returns process ID of parent of caller

3. 进程内存布局

下图展示各内存段在x86-32体系结构中布局:

 

每个进程所分配的内存有很多 部分祖晨,通常称之为段segment:

  • 文本段(text):包含了进程运行的程序机器语言指令。
  • 初始化数据段(data):包含了显式初始化的全局变量和静态变量。
  • 未初始化数据段(bss):包含了未进行显式初始化的全局变量和静态变量。
  • 栈(stack):一个动态增长和收缩的段,由栈帧(stack frames)组成。系统会为每个当前调用的函数分配一个栈帧。栈帧中存储了函数的局部变量、实参和返回值。
  • 堆(heap):可在运行时动态进行内存分配的一块区域。堆顶端称作program break。

 size可显示二进制可执行文件的文本段、初始化数据段、非初始化数据段的段大小。

PS:修改代码对比maps,是否和内存结构吻合?和size是否吻合?目的是通过maps变化,确定内存来自于何处?

4. 虚拟内存管理

虚拟内存的规划之一是将每个程序使用的内存切割成小型的、固定大小的页单元。

进程有效虚拟地址范围在其生命周期中变化可能会发生于如下场景:

  • 由于栈向下增长超出之前曾达到的位置。
  • 当在堆中分配或释放内存时,通过调用brk()、sbrk()、malloc()函数族来提升program break的位置。
  • 当调用shmat()连接System V共享内存区时,或者调用shmdt()脱离共享内存区时。
  • 当mmap()创建内存映射时,或者munmap()解除内存映射时。

5. 栈和帧

函数的调用和返回使栈的增长和收缩呈线性。每次调用函数时,会在栈上新分配一帧,每当函数返回时,再从栈上将此帧移去。

内核栈是每个进程保留在内核内存中的内存区域,在执行系统调用的过程中供内核内部函数调用使用。

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

  • 函数实参和局部变量:这些变量都是在调用函数时自动创建的,C中成为自动变量。函数返回时将自动销毁这些变量。这也是自动变量与静态全局变量主要的语义区别:后者与函数执行无关,且长期存在。
  • 函数调用的链接信息:每个函数都会用到一些CPU寄存器,会在被调用函数的栈帧中保存这些寄存器的副本。

 

6. 命令行参数(argc, argv)

每个C程序都必须有一个main()作为程序启动的起点。

执行程序时,命令行参数通过两个入参提供给main()函数。

第一个参数int argc,表示命令行参数的个数;第二个参数char *argv[]是一个指向命令行参数的指针数组,每参数都是以空字符结尾的字符串。

 

通过/proc/PID/cmdline文件可以读取任一进程的命令行参数;GNU C可使用program_invocation_name和program_invocation_short_name找到程序名称。

参数存储自己上限通过ARG_MAX限定,通过调用sysconf(__SC_ARG_MAC)确定上限值。

程序使用getopt()库函数解析命令行选项。

7. 环境列表

环境变量字符串都以名称=值形式定义。新进程在创建之时,会继承其父进程的环境副本。

可以通过export key=value来设置环境变量,也可以通过set key=value设置,通过unset key来撤销环境变量。

可以通过/proc/PID/environ检查任意进程的环境列表。

7.1 查看环境变量

C中可以通过char **environ访问环境列表。

通过指针遍历environ变量:

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

extern char **environ;
                /* Or define _GNU_SOURCE to get it from <unistd.h> */

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

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

    exit(0);
}

还可以通过int main(int argc, char *argv[], char *envp[])第三个参数访问环境列表。

getenv()从进程环境中检索单个值:

#include <stdlib.h>
char *getenv(const char *name);
    Returns pointer to (value) string, or NULL if no such variable

7.2 修改环境变量

putenv()向调用进程的环境中添加一个新变量,或者修改一个已经村侧的变量值。

#include <stdlib.h>
int putenv(char *string);
    Returns 0 on success, or nonzero on error

setenv()可以替代putenv()向环境中添加一个变量。如果name标识变量在环境中已经存在,且参数overrite值为0,则setenv()不改变环境。如果overwrite为非0,则setenv()函数总是改变环境。

#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
    Returns 0 on success, or –1 on error

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

#include <stdlib.h>
int unsetenv(const char *name);
    Returns 0 on success, or –1 on error

clearenv()清除整个环境变量。

#define _BSD_SOURCE /* Or: #define _SVID_SOURCE */
#include <stdlib.h>
int clearenv(void)
    Returns 0 on success, or a nonzero on error

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

使用库函数setjmp()和longjmp()可执行非局部跳转。非局部跳转是指跳转的目标为当前执行函数之外的某个位置。

goto跳转只能在函数内部。

#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()调用的位置。

posted on 2020-11-07 00:00  ArnoldLu  阅读(346)  评论(0编辑  收藏  举报

导航