每个进程都有一个唯一进程标识号(process ID),并保存有对其父进程号的记录。
进程的虚拟内存逻辑上被划分为许多段:文本段、数据段、栈和堆。
栈由一系列帧组成,随函数调用而增长,随函数返回而减,每个帧都包含有函数局部变量、函数实参以及单个函数调用的调用链接信息。
程序调用时命令行参数通过argc argv参数提供给main()函数。通常argv[0]包含调用程序的名称。
每个进程都会获得其父进程环境列表的一个副本,即一组”名称-值“的键值对。全局变量environ和各种库函数允许进程访问和修改其环境列表中的变量。
setjmp()函数和longjmp()函数提供了从函数甲执行非局部跳转到函数乙的方法。在调用这些函数时,为避免编译器优化所引发的问题,应使用volatile修饰符声明变量。非局部跳转会使程序难以阅读和维护,应尽量避免使用。
进程是一个可执行程序的实例。一个程序可以创建很多进程。
进程是由内核定义的抽象实体,内核为此实体分配执行程序所需的系统资源。
从内核的角度来看,进程是由用户内存空间和内核数据结构组成的。程序的代码和代码中的变量存放在用户内存空间,内核数据结构用于维护进程状态信息。
对于每个进程都有一个唯一的进程号(进程ID)(正数),用来标识系统中的某个程序。
系统调用getpid()返回调用进程的进程号。
系统调用getppid()可以检索到父进程的进程号。
使用命令pstree可以查看进程树。
如果子进程的父进程终止,则子进程就会变成孤儿,Init进程随即收养该进程,子进程后续对getppid()调用将返回进程号1.
在Linux系统中可以查看/proc/进程PID/status中PPid字段获知该进程的父进程。
本章中介绍的进程内存布局可以参考文章C内存布局,本书48.5节会更详细介绍进程内存布局。

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

char globBuf[65536];            /* Uninitialized data segment */
int primes[] = { 2, 3, 5, 7 };  /* Initialized data segment */

static int
square(int x)                   /* Allocated in frame for square() */
{
    int result;                 /* Allocated in frame for square() */

    result = x * x;
    return result;              /* Return value passed via register */
}

static void
doCalc(int val)                 /* Allocated in frame for doCalc() */
{
    printf("The square of %d is %d\n", val, square(val));

    if (val < 1000) {
        int t;                  /* Allocated in frame for doCalc() */

        t = val * val * val;
        printf("The cube of %d is %d\n", val, t);
    }
}

int
main(int argc, char *argv[])    /* Allocated in frame for main() */
{
    static int key = 9973;      /* Initialized data segment */
    static char mbuf[10240000]; /* Uninitialized data segment */
    char *p;                    /* Allocated in frame for main() */

    p = malloc(1024);           /* Points to memory in heap segment */

    doCalc(key);

    exit(EXIT_SUCCESS);
}

可以通过将上述代码中的内容只保留main函数其余注释掉之后编译,size 查看结构,然后在取消注释之后,逐步查看size的不同,进而确定该类型变量在内存中的布局.

环境列表

每个进程都有与自己相关的环境列表。
每条记录的形式为name=value;
新进程创建时会继承父进程的环境副本(原始的进程间通信方式。
可以通过printenv输出当前的环境列表:
在linux系统中通过查看/proc/进程PID/environ文件可以查看相应的环境列表。

char *getenv(const char *name);

对于存在name的环境变量就返回对应的value的指针,如果不存在就返回NULL

int putenv(char *string);

将name=value形式的字符串添加到当前的环境列表。成功返回0,失败返回非0值。
putenv是将environ变量的某个元素指向string指向的位置,并不是指向string指向字符串的副本。所以如果修改string指向的内容,环境变量也会改变,因此string指向后来不能是自动变量。
当string里不包含等号的时候,将会在环境列表移除string命名的环境变量。

int setenv(const char *name, const char *value, int overwrite);

name,value与name=value对应,overwrite决定时候改变环境。如果以name标识的变量存在环境里而且overwrite为0,就不改变环境;否则如果overwrite不为0就总是改变环境。
成功返回0, 失败返回-1
不同与putenv,setenv是分配一块内存缓冲区,将name和value指向的字符串复制到缓冲区里。

int unsetenv(const char *name);

移除name标识的变量
成功调用返回0,失败返回-1。

int clearenv(void);

成功调用返回0,失败返回非0值。
setenv和clearenv会导致内存泄露:setenv分配了一块内存缓冲区,但是clearenv没有释放这个缓冲区。
例子:

#define _GNU_SOURCE     /* Get various declarations from <stdlib.h> */
#include <stdlib.h>
#include "tlpi_hdr.h"

extern char **environ;

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

    clearenv();         /* Erase entire environment */

    /* Add any definitions specified on command line to environment */

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

    /* Add a definition for GREET if one does not already exist */

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

    /* Remove any existing definition of BYE */

    unsetenv("BYE");

    /* Display current environment */

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

    exit(EXIT_SUCCESS);
}

命令行参数相关讲解参考文章C语言命令行参数

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

setjmp()函数和longjmp()函数提供了从函数甲执行非局部跳转到函数乙的方式。
示例代码:

#include <setjmp.h>
#include "tlpi_hdr.h"

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;
    }

    exit(EXIT_SUCCESS);
}

代码解析:
上面代码保存为longjmp.c,运行./longjmp此时argc为1,如果我输入./longjmp x argc不为1,上述代码setjmp只有在第一次执行时返回0,然后下次执行时通过longjmp跳转过来,通过longjmp设置的值就是setjmp返回的值,本例中f1中返回1,f2中返回2.

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

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);
    }

    exit(EXIT_SUCCESS);
}

代码讲解:
gcc -o setjmp_vars setjmp_vars.c
gcc -O -o setjmp_vars setjmp_vars.c
-O选项是开启编译器优化,上述例子中如果不将局部变量定义为volatile的话在调用setjmp()的函数中会存在问题。所以为避免编译器优化所引发的问题,应使用volatile修饰符声明变量。