美文网首页
APUE第7章 进程环境

APUE第7章 进程环境

作者: MachinePlay | 来源:发表于2020-02-15 23:36 被阅读0次

    7.1引言

    image.png

    将介绍进程控制原语,在此之前需先了解进程的环境。本章 中将学习:当程序执行时,其main函数是如何被调用的;命令行参数是 如何传递给新程序的;典型的存储空间布局是什么样式;如何分配另外 的存储空间;进程如何使用环境变量;进程的各种不同终止方式等。另 外,还将说明longjmp和setjmp函数以及它们与栈的交互作用。本章结束 之前,还将查看进程的资源限制。

    7.2 main函数

    C程序总是从main函数开始执行。main函数的原型是:
    int main(int argc, char *argv[]); 其中,argc是命令行参数的数目,argv是指向参数的各个指针所构成的数组。7.4 节将对命令行参数进行说明。 当内核执行C程序时(使用一个exec函数,8.10节将说明exec函数),在调用main前先调用一个特殊的启动例程。可执行程序文件将此 启动例程指定为程序的起始地址——这是由连接编辑器设置的,而连接 编辑器则由C编译器调用。启动例程从内核取得命令行参数和环境变量 值,然后为按上述方式调用main函数做好安排。

    7.3 进程终止

    有8种方式使进程终止(termination),其中 5种为正常终止,它们 是:

    • (1)从main返回;
    • (2)调用exit;
    • (3)调用_exit或_Exit;
    • (4)最后一个线程从其启动例程返回(11.5节);
    • (5)从最后一个线程调用pthread_exit(11.5节)。

    异常终止有3种方式,它们是:

    • (6)调用abort(10.17节);
    • (7)接到一个信号(10.2节);
    • (8)最后一个线程对取消请求做出响应(11.5节和12.7节)。

    在第11章和第12章讨论线程之前,我们暂不考虑专门针对线程的3
    种终止方式。
    上节提及的启动例程是这样编写的,使得从main返回后立即调用 exit函数。如果将启动例程以C代码形式表示(实际上该例程常常用汇编 语言编写),则它调用main函数的形式可能是:

     exit(main(argc, argv));
    
    • 1.退出函数
      3个函数用于正常终止一个程序:_exit和_Exit立即进入内核,exit则 先执行一些清理处理,然后返回内核。
    #include <stdlib.h> 
    void exit(int status); 
    void _Exit(int status);
     
    #include <unistd.h>
    void _exit(int status);
    

    由于历史原因,exit 函数总是执行一个标准 I/O 库的清理关闭操 作:对于所有打开流调用fclose函数。回忆5.5节,这造成输出缓冲中的 所有数据都被冲洗(写到文件上)。

    3个退出函数都带一个整型参数,称为终止状态(或退出状态,exit status)。大多 数UNIX系统shell都提供检查进程终止状态的方法。如果 (a)调用这些函数时不带终止状态,或(b)main执行了一个无返回值 的return语句,或(c)main没有声明返回类型为整型,则该进程的终止 状态是未定义的。但是,若main的返回类型是整型,并且main执行到最 后一条语句时返回(隐式返回),那么该进程的终止状态是0。

    这种处理是ISO C标准1999版引入的。历史上,若main函数终止时 没有显式使用return语句或调用exit函数,那么进程终止状态是未定义 的。
    main函数返回一个整型值与用该值调用exit是等价的。于是在main 函数中
    exit(0);
    等价于
    return(0);

    #include <stdio.h>
    int main() {
        printf("hello world\n");
    }
    
    
    image.png

    -2 函数atexit

    按照ISO C的规定,一个进程可以登记多至32个函数,这些函数将 由exit自动调用。我们称这些函数为终止处理程序(exit handler),并调 用atexit函数来登记这些函数。

    #include <stdlib.h>
    int atexit(void (*func)(void));
    返回值:若成功,返回0;若出错,返回非0
    

    其中,atexit 的参数是一个函数地址,当调用此函数时无需向它传
    递任何参数,也不期望它返回一个值。exit调用这些函数的顺序与它们 登记时候的顺序相反。同一函数如若登记多次,也会被调用多次。

    ISO C要求,系统至少应支持32个终止处理程序,但实现经常会提 供更多的支持(参见图2-15)。为了确定一个给定的平台支持的最大终
    止处理程序数,可以使用sysconf函数(如图2-14所示)。

    根据ISO C和POSIX.1,exit首先调用各终止处理程序,然后关闭 (通过fclose)所有打开流。POSIX.1扩展了ISO C标准,它说明,如若 程序调用exec函数族中的任一函数,则将清除所有已安装的终止处理程 序。图7-2显示了一个C程序是如何启动的,以及它终止的各种方式。

    image.png

    注意,内核使程序执行的唯一方法是调用一个exec函数。进程自愿 终止的唯一方法是显式或隐式地(通过调用 exit)调用_exit 或_Exit。进 程也可非自愿地由一个信号使其终止(图 7-2中没有显示)。

    #include <stdio.h>
    #include <iostream>
    #include <unistd.h>
    #include <string>
    #include <stdlib.h>
    
    static void my_exit1() {
        std::cout << "my_exit" << std::endl;
    }
    static void my_exit2() {
        std::cout << "my_exit2" << std::endl;
    }
    
    int main()
    {
        atexit(my_exit1);
        atexit(my_exit2);
    
        printf("main function is done\n");
    
        return 0;
    }
    
    image.png

    7.4 命令行参数

    当执行一个程序时,调用exec的进程可将命令行参数传递给该新程 序。这是UNIX shell的一部分常规操作。在前几章的很多实例中,我们 已经看到了这一点。

    所示的程序将其所有命令行参数都回显到标准输出上。注 意,通常的 echo程序不回显第0个参数。

    #include <iostream>
    #include <stdio.h>
    
    int main(int argc ,char * argv[]) {
        int i = 0;
        for (i = 0; i < argc; ++i ) {
            printf("argv[%d] is : %s\n", argc, argv[i]);
        }
        return 0;
    }
    
    image.png

    ISO C和POSIX.1都要求argv[argc]是一个空指针。这就使我们可以将 参数处理循环改写为:

    for (int i =0 ; argv[i]!= NULL; ++i)
    

    7.5环境表

    每个程序都接收到一张环境表。与参数表一样,环境表也是一个字 符指针数组,其中每个指针包含一个以null结束的C字符串的地址。全局 变量environ则包含了该指针数组的地址:

    extern char **environ;
    

    例如,如果该环境包含5个字符串,那么它看起来如图7-5中所示。 其中,每个字符串的结尾处都显式地有一个null字节。我们称environ为 环境指针(environment pointer),指针数组为环境表,其中各指针指向 的字符串为环境字符串。


    image.png

    按照惯例,环境由
    name = value 这样的字符串组成,如图7-5中所示。大多数预定义名完全由大写字母组成,但这只是一个惯例。

    在历史上,大多数UNIX系统支持main函数带3个参数,其中第3个

    参数就是环境表地址:
    int main(int argc, char *argv[], char *envp[]);
    因为ISO C规定main函数只有两个参数,而且第3个参数与全局变量 environ相比也没有带来更多益处,所以 POSIX.1 也规定应使用 environ 而不使用第 3 个参数。通常用 getenv 和putenv函数(见7.9节)来访问特 定的环境变量,而不是用environ变量。但是,如果要查看整个环境,则 必须使用environ指针。

    我们写一个打印环境变量的代码

    #include <iostream>
    #include <stdio.h>
    
    extern char **environ;
    
    int main() {
        /*----------------------------------- display environ ----------------------------------------*/
        for (int i = 0; environ[i] != NULL; ++i) {
            printf("environ: %s\n", environ[i]);
        }
    }
    
    image.png

    7.6 C程序储存空间布局

    历史沿袭至今,C程序一直由下列几部分组成:


    image.png
    • 代码段:程序的所有指令会存放在这个区域,这是已经编译后的机器码。
      这是由CPU执行的机器指令部分。通常,正文段是可共 享的,所以即使是频繁执行的程序(如文本编辑器、C编译器和shell 等)在存储器中也只需有一个副本,另外,正文段常常是只读的,以防 止程序由于意外而修改其指令。
    • 字面量池是程序初始化时的一些字符串字面量,在程序中用于显示文字

    • 全局数据段:通常将此段称为数据段,程序初始化时的常量和全局/静态的变量。C/C++ 用global/static声明的变量都存放在这个区域,对所有函数公开可见。(static或extern 已经初始化的全局变量都在这里)
      它包含了程序中需明确 地赋初值的变量。例如, C程序中任何函数之外的声明:

       int maxcount = 99;
    

    •未初始化数据段。通常将此段称为bss段,这一名称来源于早期汇 编程序一个操作符,意思是“由符号开始的块”(block started by symbol),在程序开始执行之前,内核将此段中的数据初始化为0或空 指针。函数外的声明:

        long sum[1000];
    

    使此变量存放在非初始化数据段中。

    • 堆: 通常在堆中进行动态存储分配。由于历史上形成的惯例,堆位于未初始化数据段和栈之间。这里保存的数据只是为了临时存储一些值而创建的,而我们可能在程序运行过程中可能会回收此内存。因为我们在程序执行期间不需要很长时间,所以使用C中的new或malloc这类内存分配程序来为我们所需的特定数据类型提供新的空间,并且随着我们要求越来越多的动态数据空间而该区域不断扩大,并且在内存中逐渐增长到更高的地址。

    • 栈:
      自动变量以及每次函数调用时所需保存的信息都存放在此段 中。每次函数调用时,其返回地址以及调用者的环境信息(如某些机器 寄存器的值)都存放在栈中。然后,最近被调用的函数在栈上为其自动 和临时变量分配存储空间。通过以这种方式使用栈,C递归函数可以工 作。递归函数每次调用自身时,就用一个新的栈帧,因此一次函数调用 实例中的变量集不会影响另一次函数调用实例中的变量。

      当我们执行这些过程调用时,堆的基本特性是LIFO,存储着该程序“上下文”,它将从内存的高层地址开始,然后向另一个方向向下扩展。上下文其实就是程序中各个函数之间调用的先后顺序。

    7.7 共享库

    即动态库,linux系统上的.so文件
    现在,大多数UNIX系统支持共享库。Arnold[1986]说明了System V 上共享库的一个早期实现,Gingell等[1987]则说明了SunOS上的另一个实 现。共享库使得可执行文件中不再需要包含公用的库函数,而只需在所 有进程都可引用的存储区中保存这种库例程的一个副本。程序第一次执 行或者第一次调用某个库函数时,用动态链接方法将程序与共享库函数 相链接。这减少了每个可执行文件的长度,但增加了一些运行时间开 销。

    这种时间开销发生在该程序第一次被执行时,或者每个共享库函数 第一次被调用时。共享库的另一个优点是可以用库函数的新版本代替老 版本而无需对使用该库的程序重新连接编辑(假定参数的数目和类型都 没有发生改变)。

    在不同的系统中,程序可能使用不同的方法说明是否要使用共享 库。比较典型的有 cc(1)和ld(1)命令的选项。作为长度方面发生变化的例 子,先用无共享库方式创建下列可执行文件(典型的hello.c程序):

    $ gcc -static hello1.c     阻止gcc 使用共享库
    

    7.8 储存空间分配

    image.png

    ISO C说明了3个用于存储空间动态分配的函数。

    • (1)malloc,分配指定字节数的存储区。此存储区中的初始值不确 定。
    • (2)calloc,为指定数量指定长度的对象分配存储空间。该空间中 的每一位(bit)都初始化为0。
    • (3)realloc,增加或减少以前分配区的长度。当增加长度时,可能 需将以前分配区的内容移到另一个足够大的区域,以便在尾端提供增加的存储区,而新增 区域内的初始值则不确定。
    #include <stdlib.h>
    void *malloc(size_t size);
    void *calloc(size_t nobj, size_t size); 
    void *realloc(void *ptr, size_t newsize);
    

    3个函数返回值:若成功,返回非空指针;若出错,返回NULL

    void free(void *ptr);
    

    这3个分配函数所返回的指针一定是适当对齐的,使其可用于任何 数据对象。例如,在一个特定的系统上,如果最苛刻的对齐要求是, double必须在8的倍数地址单元处开始,那么这3个函数返回的指针都应 这样对齐。

    因为这 3 个 alloc 函数都返回通用指针 void *,所以如果在程序中包 括了#include<stdlib.h>(以获得函数原型),那么当我们将这些函数返 回的指针赋予一个不同类型的指针时,就不需要显式地执行强制类型转换。未声明函数的默认返回值为int,所以使用没有正确函数声明的强制 类型转换可能会隐藏系统错误,因为int类型的长度与函数返回类型值的 长度不同(本例中是指针)。

    函数free 释放ptr指向的存储空间。被释放的空间通常被送入可用存 储区池,以后,可在调用上述3个分配函数时再分配。

    realloc函数使我们可以增、减以前分配的存储区的长度(最常见的 用法是增加该区)。例如,如果先为一个数组分配存储空间,该数组长 度为 512,然后在运行时填充它,但运行一段时间后发现该数组原先的 长度不够用,此时就可调用 realloc 扩充相应存储空间。如果在该存储区 后有足够的空间可供扩充,则可在原存储区位置上向高地址方向扩充, 无需移动任何原先的内容,并返回与传给它相同的指针值。如果在原存 储区后没有足够的空间,则 realloc 分配另一个足够大的存储区,将现存 的512个元素数组的内容复制到新分配的存储区。然后,释放原存储 区,返回新分配区的指针。因为这种存储区可能会移动位置,所以不应 当使任何指针指在该区中。

    注意,realloc的最后一个参数是存储区的新长度,不是新、旧存储 区长度之差。作为一个特例,若ptr是一个空指针,则realloc的功能与 malloc相同,用于分配一个指定长度为newsize的存储区。

    这些函数的早期版本允许调用realloc分配自上次malloc、realloc 或calloc调用以来所释放的块。这种技巧可回溯到 V7,它利用 malloc 的搜索策略,实现存储器紧缩。Solaris仍支持这一功能,而很多其他 平台则不支持。这种功能不被赞同,不应再使用。

    这些分配例程通常用sbrk(2)系统调用实现。该系统调用扩充(或缩 小)进程的堆

    虽然sbrk可以扩充或缩小进程的存储空间,但是大多数malloc和free 的实现都不减小进程的存储空间。释放的空间可供以后再分配,但将它 们保持在malloc池中而不返回给内核。
    大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间 用来记录管理信息——分配块的长度、指向下一个分配块的指针等。这 就意味着,如果超过一个已分配区的尾端或者在已分配区起始位置之前 进行写操作,则会改写另一块的管理记录信息。这种类型的错误是灾难 性的,但是因为这种错误不会很快就暴露出来,所以也就很难发现。
    在动态分配的缓冲区前或后进行写操作,破坏的可能不仅仅是该区 的管理记录信息。在动态分配的缓冲区前后的存储空间很可能用于其他 动态分配的对象。这些对象与破坏它们的代码可能无关,这造成寻求信 息破坏的源头更加困难。
    其他可能产生的致命性的错误是:释放一个已经释放了的块;调用 free时所用的指针不是3个alloc函数的返回值等。如若一个进程调用 malloc函数,但却忘记调用free函数,那么该进程占用的存储空间就会连 续增加,这被称为泄漏(leakage)。如果不调用free函数释放不再使用 的空间,那么进程地址空间长度就会慢慢增加,直至不再有空闲空间。 此时,由于过度的换页开销,会造成性能下降。
    因为存储空间分配出错很难跟踪,所以某些系统提供了这些函数的 另一种实现版本。每次调用这3个分配函数中的任意一个或free时,它们 都进行附加的检错。在调用连接编辑器时指定一个专用库,在程序中就 可使用这种版本的函数。此外还有公共可用的资源,在对其进行编译时 使用一个特殊标志就会使附加的运行时检查生效。

    有很多可替代malloc和free的函数。某些系统已经提供替代存储空间 分配函数的库。另一些系统只提供标准的存储空间分配程序。如果需 要,软件开发者可以下载替代函数。下面讨论某些替代函数和库。

    • 1.libmalloc
      基于SVR4的UNIX系统,如Solaries,包含了libmalloc库,它提供了 一套与ISO C存储空间分配函数相匹配的接口。libmalloc库包括mallopt函 数,它使进程可以设置一些变量,并用它们来控制存储空间分配程序的 操作。还可使用另一个名为mallinfo的函数,以对存储空间分配程序的 操作进行统计。

    • 2.vmalloc
      Vo[1996]说明一种存储空间分配程序,它允许进程对于不同的存储 区使用不同的技术。除了一些vmalloc特有的函数外,该库也提供了ISO C存储空间分配函数的仿真器。

    • 3.quick-fit
      历史上所使用的标准 malloc 算法是最佳适配或首次适配存储分配策 略。quick-fit(快速适配)算法比上述两种算法快,但可能使用较多存 储空间。Weinstock和Wulf[1988]对该算法进行了描述,该算法基于将存 储空间分裂成各种长度的缓冲区,并将未使用的缓冲区按其长度组成不 同的空闲区列表。现在许多分配程序都基于快速适配。

    • 4.jemalloc
      jemalloc函数实现是FreeBSD 8.0中的默认存储空间分配程序,它是 库函数malloc族在FreeBSD中的实现。它的设计具有良好的可扩展性,可 用于多处理器系统中使用多线程的应用程序。Evans[2006]说明了具体实 现及其性能评估。

    • 5.TCMalloc
      TCMalloc函数用于替代malloc函数族以提供高性能、高扩展性和高 存储效率。从高速缓存中分配缓冲区以及释放缓冲区到高速缓存中时,
      它使用线程-本地高速缓存来避免锁开销。它还有内置的堆检查程序和 堆分析程序帮助调试和分析动态存储的使用。TCMalloc库是开源可用 的,是Google-perftools工具中的一个。Ghemawat和Menage[2005]对此做 了简单介绍。

    • 6.函数alloca
      还有一个函数也值得一提,这就是alloca。它的调用序列与malloc相 同,但是它是在当前函数的栈帧上分配存储空间,而不是在堆中。其优 点是:当函数返回时,自动释放它所使用的栈帧,所以不必再为释放空 间而费心。其缺点是:alloca 函数增加了栈帧的长度,而某些系统在函 数已被调用后不能增加栈帧长度,于是也就不能支持alloca函数。尽管如 此,很多软件包还是使用alloca函数,也有很多系统实现了该函数。
      本书中讨论的4个平台都提供了alloca函数。

    7.9环境变量

    如同前述,环境字符串的形式是:
    name=value

    UNIX内核并不查看这些字符串,它们的解释完全取决于各个应用
    程序。例如,shell使用了大量的环境变量。其中某一些在登录时自动设 置(如HOME、USER等),有些则由用户设置。我们通常在一个shell 启动文件中设置环境变量以控制shell的动作。例如,若设置了环境变量 MAILPATH,则它告诉Bourne shell、GNU Bourne-again shell和Korn shell 到哪里去查看邮件。
    ISO C定义了一个函数getenv,可以用其取环境变量值,但是该标准 又称环境的内容是由实现定义的。

    #include <stdlib.h>
    char *getenv(const char *name);
    

    返回值:指向与name关联的value的指针;若未找到,返回NULL 注意,此函数返回一个指针,它指向name=value字符串中的value。
    我们应当使用getenv从环境中取一个指定环境变量的值,而不是直接访 问environ。

    Single UNIX Specification中的POSIX.1定义了某些环境变量。如果支 持XSI扩展,那么其中也包含了另外一些环境变量定义。图7-7列出了由 Single UNIX Specification定义的环境变量,并指明本书讨论的4种实现对 它们的支持情况。由POSIX.1定义的各环境变量标记为•,否则为XSI扩 展。本书讨论的4种UNIX实现使用了很多依赖于实现的环境变量。注 意,ISO C没有定义任何环境变量。

    image.png

    除了获取环境变量值,有时也需要设置环境变量。我们可能希望改 变现有变量的值,或者是增加新的环境变量。(在下一章将会了解到, 我们能影响的只是当前进程及其后生成和调用的任何子进程的环境,但 不能影响父进程的环境,这通常是一个shell进程。尽管如此,修改环境 表的能力仍然是很有用的。)遗憾的是,并不是所有系统都支持这种能 力。图7-8列出了由不同的标准及实现支持的各种函数。

    image.png

    clearenv不是Single UNIX Specification的组成部分。它被用来删除环 境表中的所有项。在图7-8中,中间3个函数的原型是:

    #include <stdlib.h> 
    int putenv(char *str);
    函数返回值:若成功,返回0;若出错,返回非0
    int setenv(const char *name, const char *value, int rewrite);
    int unsetenv(const char *name);
    两个函数返回值:若成功,返回0;若出错,返回−1 这3个函数的操作如下。
    
    
    • putenv取形式为name=value的字符串,将其放到环境表中。如果 name已经存在,则先删除其原来的定义。

    • setenv将name设置为value。如果在环境中name已经存在,那么 (a)若rewrite非0,则首先删除其现有的定义;(b)若rewrite为0,则 不删除其现有定义(name不设置为新的value,而且也不出错)。

    • unsetenv删除name的定义。即使不存在这种定义也不算出错。
      注意,putenv和setenv之间的差别。setenv必须分配存储空间,以 便依据其参数创建name=value字符串。putenv可以自由地将传递给它的 参数字符串直接放到环境中。确实,许多实现就是这么做的,因此,将 存放在栈中的字符串作为参数传递给putenv就会发生错误,其原因是, 从当前函数返回时,其栈帧占用的存储区可能将被重用。

    我们写一个获取HOME目录环境变量的小代码

    #include <iostream>
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    
    int main() {
        /*----------------------------------- test getenv ----------------------------------------*/
        
        /* env name. */
        char name[255];
        memset(name, 0, 255);
        char *p = name;
    
        /* get env. */
        p = getenv("HOME");
    
        printf("env HOME: %s\n", p);
    }
    
    image.png

    这些函数在修改环境表时是如何进行操作的呢?对这一问题进行研 究、考察是非常有益的。回忆程序的内存布局,其中,环境表(指向实际 name=value字符串的指针数组)和环境字符串通常存放在进程存储空间 的顶部(栈之上)。删除一个字符串很简单——只要先在环境表中找到 该指针,然后将所有后续指针都向环境表首部顺次移动一个位置。但是 增加一个字符串或修改一个现有的字符串就困难得多。环境表和环境字符串通常占用的是进程地址空间的顶部,所以它不能再向高地址方向 (向上)扩展:同时也不能移动在它之下的各栈帧,所以它也不能向低 地址方向(向下)扩展。两者组合使得该空间的长度不能再增加。

    (1)如果修改一个现有的name:

    • a.如果新value的长度少于或等于现有value的长度,则只要将新字 符串复制到原字符串所用的空间中;

    • b.如果新value的长度大于原长度,则必须调用malloc为新字符串分 配空间,然后将新字符串复制到该空间中,接着使环境表中针对name的 指针指向新分配区。

    (2)如果要增加一个新的name,则操作就更加复杂。首先,必须 调用malloc为name=value字符串分配空间,然后将该字符串复制到此空 间中。

    • a.如果这是第一次增加一个新name,则必须调用malloc为新的指针 表分配空间。接着,将原来的环境表复制到新分配区,并将指向新 name=value字符串的指针存放在该指针表的表尾,然后又将一个空指针 存放在其后。最后使environ指向新指针表。再看一下图7-6,如果原来 的环境表位于栈顶之上(这是一种常见情况),那么必须将此表移至堆 中。
      但是,此表中的大多数指针仍指向栈顶之上的各name=value字符 串。

    • b.如果这不是第一次增加一个新name,则可知以前已调用malloc在 堆中为环境表分配了空间,所以只要调用 realloc,以分配比原空间多存 放一个指针的空间。然后将指向新name=value字符串的指针存放在该表 表尾,后面跟着一个空指针。

    7.10 setjmp 和 long jmp

    在C中,goto语句是不能跨越函数的,而执行这种类型跳转功能的 是函数setjmp和longjmp。这两个函数对于处理发生在很深层嵌套函数调 用中的出错情况是非常有用的。

    非局部goto——setjmp和longjmp函 数。非局部指的是,这不是由普通的C语言goto语句在一个函数内实施 的跳转,而是在栈上跳过若干调用帧,返回到当前函数调用路径上的某 一个函数中。

    #include <setjmp.h>
    int setjmp(jmp_buf env);
    返回值:若直接调用,返回0;若从longjmp返回,则为非0
    void longjmp(jmp_buf env, int val);
    

    setjmp参数env的类 型是一个特殊类型jmp_buf。这一数据类型是某种形式的数组,其中存 放在调用 longjmp 时能用来恢复栈状态的所有信息。因为需在另一个函 数中引用env变量,所以通常将env变量定义为全局变量。
    要保证env不会因为栈帧切换改变,可以保存为static /extern 或在堆区建设

    7.11 函数getrlimit 和setrlimit

    每个进程都有一组资源限制,其中一些可以用getrlimit和setrlimit函 数查询和更改。

    #include <sys/resource.h>
    int getrlimit(int resource, struct rlimit *rlptr);
    int setrlimit(int resource, const struct rlimit *rlptr);
    

    两个函数返回值:若成功,返回0;若出错,返回非0
    这两个函数在Single UNIX Specification的XSI扩展中定义。进程 的资源限制通常是在系统初始化时由0进程建立的,然后由后续进程继 承。每种实现都可以用自己的方法对资源限制做出调整。

    对这两个函数的每一次调用都指定一个资源以及一个指向下列结构 的指针。

    struct rlimit {
    rlim_t rlim_cur; /* soft limit: current limit */
    rlim_t rlim_max; /* hard limit: maximum value for rlim_cur */ };
    

    在更改资源限制时,须遵循下列3条规则。
    (1)任何一个进程都可将一个软限制值更改为小于或等于其硬限 制值。
    (2)任何一个进程都可降低其硬限制值,但它必须大于或等于其 软限制值。这种降低,对普通用户而言是不可逆的。
    (3)只有超级用户进程可以提高硬限制值。

    常量RLIM_INFINITY指定了一个无限量的限制。
    这两个函数的 resource 参数取下列值之一。图 7-15 显示哪些资源限 制是由 Single UNIX Specification定义并由本书讨论的4种UNIX系统实现 支持的。


    image.png
    RLIMIT_AS 进程总的可用存储空间的最大长度(字节)。这影响 到 sbrk 函数(1.11节)和mmap函数(14.8节)。
    
    RLIMIT_CORE core文件的最大字节数,若其值为0则阻止创建core 文件。
    
    RLIMIT_CPU CPU时间的最大量值(秒),当超过此软限制时,向 该进程发送SIGXCPU信号。
    
    RLIMIT_DATA 数据段的最大字节长度。这是始化数据、非初始以及堆的总和。
    
    RLIMIT_FSIZE 可以创建的文件的最大字节长度。当超过此软限制时,则向该进程发送SIGXFSZ信号。
    
    RLIMIT_MEMLOCK 一个进程使用mlock(2)能够锁定在存储空间中
    的最大字节长度。
    
    RLIMIT_MSGQUEUE 进程为POSIX消息队列可分配的最大存储字
    节数。
    
    RLIMIT_NICE 为了影响进程的调度优先级,nice值(8.16节)可设
    置的最大限制。
    
    RLIMIT_NOFILE 每个进程能打开的最多文件数。
    
    RLIMIT_NPROC 每个实际用户 ID 可拥有的最大子进程数。更改此 限制将影响到sysconf函数在参数_SC_CHILD_MAX中返回的值(见2.5.4 节)。
    
    RLIMIT_SWAP 用户可消耗的交换空间的最大字节数 
    
    RLIMIT_VMEM 这是RLIMIT_AS的同义词。
    

    资源限制影响到调用进程并由其子进程继承。这就意味着,为了影
    响一个用户的所有后续进程,需将资源限制的设置构造在shell之中。确 实,Bourne shell、GNU Bourne-again shell和Korn shell具有内置的ulimit命令,C shell具有内置limit命令。(umask和chdir函数也必须是shell内置 的。)

    打印由系统支持的所有资源当前的软限制和硬限制。 为了在各种实现上编译该程序,我们已经条件地包括了各种不同的资源 名。

    #include <iostream>
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/resource.h>
    
    rlimit my_rlimit;
    
    int main() {
        /*----------------------------------- get cpu rlimit ----------------------------------------*/
        int ret = getrlimit(RLIMIT_CPU, &my_rlimit);
        printf("cpu   rlimit: (%llu)  (%llu)\n", my_rlimit.rlim_cur, my_rlimit.rlim_max);
        
        /*----------------------------------- get nire rlimit ----------------------------------------*/
        ret = getrlimit(RLIMIT_AS, &my_rlimit);
        printf("AS    rlimit: (%llu)  (%llu)\n", my_rlimit.rlim_cur, my_rlimit.rlim_max);
    
        /*----------------------------------- get stack rlimit ----------------------------------------*/
        ret = getrlimit(RLIMIT_STACK, &my_rlimit);
        printf("stack rlimit: (%llu)  (%llu)\n", my_rlimit.rlim_cur, my_rlimit.rlim_max);
    
        return 0;
    }
    
    image.png

    也可以通过系统命令行来修改硬上限

    • linux可以通过ulimit命令查看栈上限和设置上限
    ulimit -a 查看进程所有资源上限
    ulimit -s xx 修改栈上限
    

    7.12 小节

    理解UNIX系统环境中C程序的环境是理解UNIX系统进程控制特性 的先决条件。本章说明了一个进程是如何启动和终止的,如何向其传递 参数表和环境。虽然参数表和环境都不是由内核进行解释的,但内核起 到了从exec的调用者将这两者传递给新进程的作用。
    本章也说明了C程序的典型存储空间布局,以及一个进程如何动态 地分配和释放存储空间。详细地了解用于维护环境的一些函数是有意义 的,因为它们涉及存储空间分配。本章也介绍了setjmp 和 longjmp 函 数,它们提供了一种在进程内非局部转移的方法。最后介绍了各种实现 提供的资源限制功能。

    相关文章

      网友评论

          本文标题:APUE第7章 进程环境

          本文链接:https://www.haomeiwen.com/subject/wzgjfhtx.html