美文网首页LinuxLinux学习之路
APUE读书笔记-07进程环境(4)

APUE读书笔记-07进程环境(4)

作者: QuietHeart | 来源:发表于2020-05-13 08:11 被阅读0次

    8、内存分配

    ISO C 指定了三种内存分配的方式:

    #include <stdlib.h>
    void *malloc(size_t size);
    void *calloc(size_t nobj, size_t size);
    void *realloc(void *ptr, size_t newsize);
    

    这三种函数如果返回的指针是非空的表示分配成功,如果出错了那么返回的是 null .

    void free(void *ptr);
    
    • malloc 分配指定字节大小的内存,内存里面的数据是不确定的。

    • calloc 分配 nobj 个特定大小( size )的对象,内存里面的数据被初始化为0。

    • realloc 用来增加或者减小之前分配的内存区域。如果是增加的话,可能会把原先的分配的区域移动到别的地方然后在后面增加多出来的内存,增加的内存的内容不确定并返回新内存块的指针,如果原来的区域后面有足够的空间来增加额外的内存的话就不需要移动内存了。参数是最终内存的总大小。如果 ptr 为空那么 realloc 的行为和 malloc 的行为是一样的。

      三种分配函数返回的指针的对齐方式保证适合任何的数据类型。因为函数返回的是 void* 所以如果我们包含了 stdlib.h 那么我们不用强制将这个函数返回的指针转换成为我们想要的类型。

    • free 使用来释放之前分配的内存的。

      分配函数是使用 sbrk 系统来进行实现的。实际上分配的空间要比指定的空间大一些,因为需要存放空间大小等信息让 free 正确地执行。如果只分配不释放会导致内存泄露,如果释放了没有分配的内存,那么会导致致命的错误。

    由于这些函数导致的内存错误很难跟踪,所以有些系统提供了这些函数的可以进行一些额外检测的版本(当 alloc 或者 free 时)。可以通过包含一些库文件或者有时候也可以通过特定的编译选项来打开这些额外检查的特性。

    一些其他的内存分配函数:

    除了前面提到的 malloc , free 等标准分配函数,还有一些替代的内存分配函数。有些系统只提供了标准的分配函数,开发者可以自己选择下载其他的替代分配函数来使用。

    1. libmalloc

      SVR4-based 的系统,例如 solaris ,包含 libmalloc 库,这个库提供了一系列的和 ISO C 内存分配函数相匹配的接口。 libmalloc 库包括 mallopt 函数,这个函数允许进程设置特定的变量来控制存储的分配(???)。还有一个函数叫做 mallinfo ,可以用来提供内存分配器的统计信息。

    2. vmalloc

      这是一种允许进程对不同的内存区域使用不同的技术分配内存的内存分配器。出了 vmalloc 之外,这个库也提供了 ISO C 内存分配函数的模拟版本。

    3. quick-fit

      以前标准的分配算法使用的是最佳适配或者最先适配分配策略。 Quick-fit 比这两种方法都快,但是它会消耗更多的内存。这个算法基于把内存分成不同大小的缓存块,然后基于缓存的大小,在 free list 上面维护没有使用的缓存。在一些 ftp 站点上面有基于 quick-fit 实现的 mallocfree

    4. alloca Function

      还有一个值得一提的函数就是 alloca .这个函数和 malloc 具有一样的调用次序,然而它不是从 heap 上面分配内存而是从当前函数所处堆栈上面分配内存的。这样的有时就是我们不用释放空间了,当函数结束的时候内存的空间会自动地被释放掉。 alloca 函数导致堆栈桢的大小增加。这样的缺点是有些系统不支持 alloca 函数,因为对于它们而言不可能在调用函数之后再增加堆栈桢的大小。然而许多的软件包还是使用了它,因为这个函数在许多系统上都有实现。

    译者注

    下面是一个使用 sbrk 的例子:

    /*测试用sbrk分配内存
     * #include <unistd.h>
     * int brk(void *addr);
     * void *sbrk(intptr_t increment);
     * brk和sbrk用来改变程序数据段的结尾地址。如果地址增加了表示为这个进程分配内存了,如果地址减少了表示给这个进程释放内存了。
     * sbrk把程序的结尾地址增加increment,如果increment的值为0会返回当前程序的结尾地址。返回先前的程序结尾,如果之前已经增加过那么返回上次的新的程序结尾(不是设置完了之后的),失败了就返回-1,同时errno被设置为ENOMEM.
     *
     * 一个可能的输出结果是:
     * current break is:153415680
     * after add break is:153415680
     * after sub break is:153415690
     * current break is:153415680
     *
     * malloc就是用这个系统调用实现的。
     * */
    #include <stdio.h>
    #include <unistd.h>
    int main(int argc, char *argv[])
    {
            void *cur = sbrk(0);
            printf("current break is:%d\n",(int)cur);//
    
            void *newadd1 = sbrk(10);
            printf("after add break is:%d\n",(int)newadd1);//
            void *newsub1 = sbrk(-10);
            printf("after sub break is:%d\n",(int)newsub1);//
    
            cur = sbrk(0);
            printf("current break is:%d\n",(int)cur);//
    
            return 0;
    }
    

    原文参考

    参考: APUE2/ch07lev1sec8.html

    9、再谈环境变量

    我们前面说了环境变量一般的形式是:"name=value",unix内核从来不会检查这些字符串,对于这些字符串的解释让程序自己来做。shell中就维护了大量的内部变量例如:HOME,USER,MAILPATH等等。

    ISO C定义了一些可以获取环境变量值的函数,但是标准说值的内容是由实现定义的。

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

    返回和name相关的环境变量的值的指针,如果没有这个环境变量那么返回NULL。需要注意的是这个函数返回"name=value"字符串。我们一般调用getenv来获得特定的值,而不是直接访问environ。有些环境变量是特定的标准(例如XSI)才有的,而不是所有的标准才有这样的环境变量。

    有时候我们想要修改一个环境变量或者添加一个环境变量,但是并不是所有的系统都支持这样的功能。下面的函数就和这个相关:

    #include <stdlib.h>
    int putenv(char *str);
    int setenv(const char *name, const char *value,int rewrite);
    int unsetenv(const char *name);
    

    putenv函数以一个"name=value"字符串做为输入,把这个字符串放到环境列表中去。如果name已经存在了,那么旧的定义会被移除。

    setenv函数设置name为value,如果name已经存在了,那么

    1. 如果重写的值非0,会先把已经定义的name给移除;
    2. 如果重写的值为0,那么已经存在的name定义不会被移除,name也不会被设置为新值也不会有任何错误发生。

    unsetenv函数会把任何定义的name给移除。如果没有相应的定义也不会出现错误。

    注意putenv和setenv的不同之处。setenv必须分配一块memory来创建name=value来根据参数创建字符串,putenv直接把传递给它的字符串自由地传递到environment.在linux和solaris中,putenv的实现会把我们传递给它的地址直接放到environment list中去。这时候,如果传递一个在stack上面的字符串将会出现错误。

    修改环境变量列表的操作实际很复杂。前面已经说过,环境变量列表是指针数组,指针元素指向了实际的"name=value"字符串。环境变量字符串一般都存放在进程内存空间的顶部,在对阵的上面。如果删除一个字符串很简单,我们只需要找到该字符串在环境变量列表中相应的指针,然后把所有后面的指针向下移动一个单元就行了。但是如果添加一个字符串或者修改已经存在的字符串就很困难了。堆栈顶部的空间是不能被扩展的,因为它一直在进程地址空间的顶部,并且不能向上扩展;它也不能向下扩展,因为下面的stack不能被move。

    如果我们修改一个已经存在的名字:

    1. 如果新value的size比原来的小或者等于原来的value,那么我们可以把新的字符串拷贝替换旧的字符串。
    2. 如果新value比旧大,我们必须为新的字符串分配空间,把新的字符串拷贝到相应的空间,然后把环境变量列表中相应的指针修改成指向新分配的空间。

    如果我们想要增加一个环境变量,那么情况更为复杂.首先,我们的调用malloc为"name=value"分配空间,然后把字符串拷贝到这个空间。

    1. 然后,如果是我们第一次添加新的变量,我们需要调用malloc分配一个新的指针列表。我们把旧的环境变量指针列表内容拷贝到这个新的列表里面,然后把我们定义的新的环境变量的地址放到这个列表指针的最后。注意,如果原来的环境变量列表是在堆栈的顶部,我们需要把指针列表移动到heap中去。但是大多数在这个堆栈上面的列表的指针还是指向了name=value字符串。
    2. 如果我们不是第一次将新的环境变量添加到环境变量列表中去,那么我们需要知道我们已经在heap中为这个列表分配了空间,所以我们只需要调用realloc来分配可以容纳更多指针的空间。这个指向新定义环境变量的指针存放在列表的结尾,其后是null指针。

    实际我们修改环境变量的时候,只能影响当前进程以及当前进程的子进程的环境变量。

    原文中的参考资料中,给出了有关这些函数,以及相应的一些环境变量在系统上面的支持。这里不再给出,需要查看的读者可以从下面给出的参考网址中找到相应的内容。

    译者注

    原文参考

    参考: APUE2/ch07lev1sec9.html

    10、关于跳转函数

    在c语言中,我们不能够使用goto跳转到另外一个函数的标签中去,如果我们想要实现这样的功能,我们需要使用setjmp和longjmp。

    #include <setjmp.h>
    int setjmp(jmp_buf env);
    void longjmp(jmp_buf env, int val);
    

    我们需要在跳转的地方调用setjmp,其参数保存了跳转时候的各种信息,并且因为是从不同函数的跳转所以一般是全局变量。

    setjmp的返回值,直接调用它返回的是0,通过longjmp返回的,是非0。

    我们在需要跳转的时候,调用longjmp函数,这个函数有两个参数,一个是先前设置setjmp的变量,一个是用来设置跳转之后setjmp的返回值。

    一个使用这两个函数的例子:

    #include <setjmp.h>
    jmp_buf jmpbuffer;
    int main(void)
    {
            ...
            if (setjmp(jmpbuffer) != 0)
                    printf("error");
            while (fgets(line, MAXLINE, stdin) != NULL)
                    do_line(line);
            exit(0);
    }
     ...
    void cmd_add(void)
    {
        int     token;
            ...
        if (token < 0)    /* an error has occurred */
            longjmp(jmpbuffer, 1);
        /* rest of processing for this command */
    }
    

    当main函数执行的时候,我们调用setjmp,这个函数记录跳转时候需要保存的信息,之后返回0。当我们调用do_line的时候,do_line会调用cmd_add,然后cmd_add调用logjmp,调用logjmp导致跳转到了main调用setjmp的地方,并且setjmp返回值变为longjmp的第2个参数指定的1.

    跳转的时候哪些变量保存?哪些变量不保存?假设setjmp之前定义了变量并且赋值,再setjmp,之后再修改变量值;之后,调用某些函数导致longjmp。这时候,longjmp将返回setjmp的位置,我们这里假设关注的变量有5个种类:全局变量,静态变量,寄存器变量,自动变量,volatile变量,一般来说,具体会保存那些变量,以来于具体情况。多数的实现不会将自动变量和寄存器变量恢复,但是标准说他们的值是不确定的。如果你有一个自动变量,并且你不想让它在跳转的时候被回滚,那么应该把它定义成为volatile属性。全局变量和静态变量的值在进行longjmp的时候不会回滚。具体参见参考资料里面的例子。从例子中看,跳转之后确定不被恢复的是全局,静态和volatile变量。

    译者注

    原文参考

    参考: APUE2/ch07lev1sec10.html

    11、资源的限制

    每个进程都有各种资源的限制,使用下列函数可以获得当前进程的各种资源的限制:

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

    这两个函数属于XSI扩展以及Single Unix标准。进程资源的限制是在系统初始化的时候被进程0来建立起来的。每种实现的资源种类可能会有所不同。

    资源的结构有两个成员:软资源限制;硬资源限制。修改资源的限制遵循如下的规则:

    1. 进程可以在小于等于硬资源限制的前提下修改软资源限制。
    2. 进程可以减少它的硬资源限制,硬资源限制不能小于软资源限制,普通用户减少硬资源限制之后不能够反向修改了(也就是增加了).
    3. 只有超级用户可以提高硬资源的限制。

    资源限制影响当前的进程以及它的子进程。这也就是说设置资源的应该编译到shell中去以便影响我们的所有进程。例如命令:ulimit或者c shell的limit命令。更多的信息,参见下面网址中给出的参考资料。

    译者注

    原文参考

    参考: APUE2/ch07lev1sec11.html

    12、总结

    在知道UNIX系统中进程控制相关的特性之前,需要对UNIX系统环境中,一个C程序的运行环境有所了解。本章我们看到如何启动和终止进程,以及如何传递参数列表和环境变量。尽管两者在内核中并不知道这些,但是调用者调用exec,执行新的程序,却是通过内核传递这些的。

    我们也看到了一个C程序的典型内存布局,以及一个进程如何能够动态分配和释放内存。因为涉及到了内存的分配问题,所以很有必要来详细对这些操作环境变量的函数。我们也看到了setjmp和longjmp函数,它们提供了一个在进程中实现非本地化程序分支(跳转)的方法。我们最后通过介绍各种系统实现提供的资源限制,来结束本章。

    译者注

    原文参考

    参考: APUE2/ch07lev1sec12.html

    相关文章

      网友评论

        本文标题:APUE读书笔记-07进程环境(4)

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