第7章 进程环境

  1. main函数
    C程序总是从main函数开始执行。main函数的原型是:
    int mian(int argc, char *argv[]);
    其中,argc是命令行参数的数目,argv是指向参数的各个指针所构成的数组
  2. 进程终止
    有8种方式使进程终止,其中5种为正常终止,它们是:
    1)从main返回
    2) 调用exit
    3)调用_exit或者_Exit
    4)最后一个线程从其启动历程返回
    5) 从最后一个线程调用pthread_exit
    异常终止有3种方式,它们是:
    6)调用abort
    7)接到一个信号
    8)最后一个线程对取消请求做出响应

    3个函数用于正常终止一个程序:_exit 和 _Exit 立即进入内核,exit则先执行一些清理处理,然后返回内核

    按照ISOC的规定,一个进程可以登记多至32个函数,这些函数将由exit自动调用。我们称这些函数为终止处理程序,并调用atexit函数来登记这些函数。
    #include<stdlib.h>
    int atexit(void (*func)(void));
    若成功,返回0,不成功返回非0

    exit调用这些函数的的顺序与它们登记时候的顺序相反。同一函数如若登记多次,也会被调用多次。
    exit首先调用各终止处理程序,然后关闭(通过fclose)所有打开流。

    注意:内核使程序执行的唯一方法是调用一个exec函数。进程自愿终止的唯一方法时显示或隐式得(通过调用exit)调用_exit或_Exit


    在调用return后再执行相应函数
  3. 命令行参数
    int main(int argc,char *argv[])
    C语言约定argv[0]储存的是程序名,所以argc至少为1。
    而且argv[argc]的值是空指针。
    #include<stdio.h>
    #include<stdlib.h>
    int main(int argc, char *argv[])
    {
        for(int i = 0; i < argc; ++i)
            printf("%s ",argv[i]);
        system("pause");
        return 0;
    }
    执行./a.exe  a b c
    ./a.exe
     a
     b
     c
  4. 环境表
    在每个进程启动时,都会接到一张环境表。环境表是一个字符指针数组,其中每个指针包含一个以 null 结束的 C 字符串的地址。全局变量environ 则包含了该指针数组的地址,
    extern char **environ;
    例如,图 7-5 显示了包含有 5 个环境字符串的环境表,

    环境字符串的形式通常为,name=value。
    ISO C 定义了一个函数 getenv,用于获取环境变量值。
    #include <stdlib.h>
    
    char *getenv(const char *name);
    
    返回值:指向与name关联的value的指针,若为找到则返回NULL
    POSIX.1 定义了两个函数 putenv 和 setenv,用于设置环境变量,
    #include <stdlib.h>
    
    int putenv(char *str);
    
    返回值:若成功则返回0,若出错则返回非0值
    putenv 的参数是形式为 name=value 的字符串的指针,将参数放到环境表中。如果 name 已经存在,则先删除其原有的定义
    #include <stdlib.h>
    
    int setenv(const char *name, const char *value, int rewrite);
    
    返回值:若成功则返回0,若出错则返回-1
    setenv 函数将环境变量 name 的值设置为 value。如果环境表中 name 已存在,那么
    • 若 rewrite 为非 0 值,则首先删除其现有定义。
    • 若 rewrite 为 0,则不删除其现有定义,name 不设置为新的 value,也不出错。
    另外提供了 unsetenv 函数用于删除环境变量,
    #include <stdlib.h>
    
    int unsetenv(const char *name);
    
    返回值:若成功则返回0,若出错则返回-1
    unsetenv 函数删除 name 的定义,即使不存在环境变量 name 也不出错。
    样例:
    #include <stdio.h>
    #include <stdlib.h>
    int
    main(void)
    {
        char *envVal;
        char *myenv;
        if ((envVal = getenv("PATH")) == NULL) {
            printf("not environment variable PATH\n");
        } else {
            printf("PATH=%s\n", envVal);
        }
        if (setenv("myenv", "li enhua", 0) == -1) {
            printf("setenv error\n");
         } else {
            myenv = getenv("myenv");
            printf("myenv=%s\n", myenv);
        }
        if (putenv("myenv1=haha") != 0) {
             printf("putenv error\n");
        } else {
             myenv = getenv("myenv1");
             printf("myenv1=%s\n", myenv);
        }
        exit(0);
    }
    编译该程序,生成文件 envdemo,然后运行该文件,
    lienhua34:demo$ gcc -o envdemo envdemo.c
    lienhua34:demo$ ./envdemo
    PATH=/usr/local/texlive/2013/bin/i386-linux:/usr/local/texlive/2013/bin/i386-linux:/
    myenv=li enhua
    myenv1=haha
  5. c程序的存储空间布局
     下面是对可执行文件aa运行size命令后得到的结果。
         [root@xxx1 algriom]# size aa
        text      data    bss   dec    hex   filename
        1257  500     16   1773    6ed  aa
        其中:
        text:表示正文段大小,这是CPU执行的机器指令部分,通常,正文段是可共享的,所以即使是频繁执行的程序在存储器中也只需有一个副本;
        data:表示包含静态变量和已经初始化(可执行文件包含了初始化的值)的全局变量的数据段大小;
        bss :表示由可执行文件中不含其初始化值的全局变量组成,称之为未初始化数据段;     
        PS:第4列和第5列是分别以十进制和十六进制表示的三个段的总长度。
    BSS段:BSS段(bss segment)通常是指用来存放程序中未初始化的全局变量的一块内存区域。BSS的英文是Block Started by Symbol的简称。BSS段属于静态内存分配。
    数据段:数据段(data segment)通过是指用来存放程序已经初始化的全局变量的一块内存区域。数据段属于静态内存分配。
    代码段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读,某些架构也允许代码段为可写,即允许修改程序。在代码段中,也有可能包含一些只读的常数变量,比如字符串常量等。
     堆:堆(heap)是用于存放进程运行中动态分配的内存段,它的大小并不固定,可动态扩张或缩减。当进程调用malloc等函数分配内存的时候,新分配的内存就被动态添加到堆上(堆被扩张);当利用free等函数释放内存的时候,被释放的内存从堆中被剔除(堆被缩减)。
    栈:栈(stack)又称堆栈,是用户存放程序临时创建的局部变量,也就是我们函数括弧{}中定义的比变量(但不包括static声明的静态变量,static意味着在数据段中存放的变量)。除此之外,在函数被调用时,其参数也会被压入发起调用的进程栈中,并且等到调用结束后,函数的返回值也会被存放回栈中。由于栈先进后出的特点,所以栈特别方便用来保存/恢复调用现场。从这个意义上讲,我们可以把堆栈看成一个寄存、交换临时数据的内存区。
        可以看到一个可执行程序在存储(没有调入内存)时分为代码段、数据区和未初始化数据区三部分。
        (1)代码区:也称之为正文段,这是存放CPU执行的机器指令。通常代码区是共享的,即其它执行程序可以调用它。代码段(code segment/text segment)通常是只读的,有些架构也允许自行修改。
        (2)数据区:存放已经初始化的全局变量、已经初始化的静态变量(包括全局和局部的)。
        (3)未初始化数据区(Block Started by Symbol,BSS)存放未初始化的全局变量和未初始化的静态变量。BSS的数据在程序开始执行之前被初始化为0或NULL。
        代码区所在的地址空间最低,往上依次是数据区和BSS区,并且数据区和BSS区在内存中是紧挨着的。
        典型的存储器安排,入下图所示。

    text段和data段在编译时已经分配了空间,而BSS段并不占用可执行文件的大小,它是由链接器来获取内存的。
        bss段(未进行初始化的数据)的内容并不存放在磁盘上的程序文件中。其原因是内核在程序开始运行前将它们设置为0。需要存放在程序文件中的只有正文段和初始化数据段。
        data段(已经初始化的数据)则为数据分配空间,数据保存到目标文件中。
        数据段包含经过初始化的全局变量以及它们的值。BSS段的大小从可执行文件中得到,然后链接器得到这个大小的内存块,紧跟在数据段的后面。当这个内存进入程序的地址空间后全部清零。包含数据段和BSS段的整个区段此时通常称为数据区。
        可执行程序在运行时又多出两个区域:栈区和堆区。
        (4)栈区:由编译器自动释放,存放函数的参数值、局部变量等。每当一个函数被调用时,该函数的返回类型和一些调用的信息被存放到栈中。然后这个被调用的函数再为他的自动变量和临时变量在栈上分配空间。每调用一个函数一个新的栈就会被使用。栈区是从高地址位向低地址位增长的,是一块连续的内存区域,最大容量是由系统预先定义好的,申请的栈空间超过这个界限时会提示溢出,用户能从栈中获取的空间较小。
        (5)堆区:用于动态分配内存,位于BSS和栈中间的地址区域。由程序员申请分配和释放。堆是从低地址位向高地址位增长,采用链式存储结构。频繁的malloc/free造成内存空间的不连续,产生碎片。当申请堆空间时库函数是按照一定的算法搜索可用的足够大的空间。因此堆的效率比栈要低的多。
        注意:a.out中还有若干其他类型的段,例如,包含符号表的段、包含调试信息的段以及包含动态共享库链接表的段等等。这些部分并不装载到进程执行的程序映像中。
    举个例子说明各种变量存在什么区:
    int a=0; //a在全局已初始化数据区 
    
    char *p1; //p1在BSS区(未初始化全局变量) 
    
    main()
    
    {
    
        int b; //b为局部变量,在栈区 
    
        char s[]="abc"; //s为局部数组变量,在栈区 
    
        //"abc"为字符串常量,存储在已初始化数据区 
    
        char *p1,*p2; //p1,p2为局部变量,在栈区 
    
        char *p3="123456"; //p3在栈区,123456\0在已初始化数据区 
    
        static int c=0; //c为局部(静态)数据,在已初始化数据区 
    
        //静态局部变量会自动初始化(因为BSS区自动用0或NULL初始化)
    
        p1=(char*)malloc(10); //分配得来的10个字节的区域在堆区 
    
        p2=(char*)malloc(20); //分配得来的20个字节的区域在堆区 
    
        free(p1);
    
        free(p2);
    
        p1=NULL; //显示地将p1置为NULL,避免以后错误地使用p1
    
        p2=NULL;
    
    }
  6. 共享库
    共享库使得可执行文件中不再需要包含公用的库函数,而只需在所有进程都可引用的存储区中保存这种库例程的一个副本。程序第一次
    执行或者第一次调用某个库函数时,用动态链接方法将程序与共享库函数相连接。这减少了每个可执行文件的程度,但增加了一些运行
    时间开销。这种时间开销发生在改程序第一次被执行时,或者每个共享库第一次被调用时。共享库的另一个优点是可以用库函数的
    新版本代替老版本而无需对使用该库的程序重新链接编辑

  7. 存储空间分配
    ANSI C说明了三个用于存储空间动态分配的函数
    (1) malloc   分配指定字节数的存储区。此存储区中的初始值不确定
    (2) calloc   为指定长度的对象,分配能容纳其指定个数的存储空间。该空间中的每一位(bit)都初始化为0
    (3) realloc  更改以前分配区的长度(增加或减少)。当增加长度时,可能需将以前分配区的内容移到另一个足够大的区域,而新增区域内的初始值则不确定
    分配函数时再分配 realloc()
    使我们可以增、减以前分配区的长度(最常见的用法是增加该区)。
    如果先分配一个可容纳长度为512的数组的空间,并在运行时填充它,但又发现空间不够,则可调用realloc扩充该存储空间。
    如果在该存储区后有足够的空间可供扩充,则可在原存储区位置上向高地址方向扩充,并返回传送给它的同样的指针值。
    如果在原存储区后没有足够的空间,则realloc分配另一个足够大的存储区,将现存的元素数组的内容复制到新分配的存储区。
    因为这种存储区可能会移动位置,所以不应当使用任何指针指在该区中。
    注意,realloc的最后一个参数是存储区的newsize(新长度),不是新、旧长度之差。作为一个特例,若ptr是一个空指针,则realloc的功能与malloc相同,用于分配一个指定长度newsize的存储区。
    这些分配例程通常通过sbrk(2)系统调用实现。该系统调用扩充(或缩小)进程的堆。虽然sbrk可以扩充或缩小一个进程的存储空间,但是大多数 malloc和free的实现都不减小进程的存储空间。释放的空间可供以后再分配,但将它们保持在malloc池中而不返回给内核。
    应当注意的是,大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记录管理信息——分配块的长度,指向下一个分配块的指针等等。这就 意味着如果写过一个已分配区的尾端,则会改写后一块的管理信息。这种类型的错误是灾难性的,但是因为这种错误不会很快就暴露出来,所以也就很难发现。
    将指向分配块的指针向后移动也可能会改写本块的管理信息。其他可能产生的致命性的错误是:释放一个已经释放了的块;调用free时所用的指针不是三 个alloc函数的返回值等。因为存储器分配出错很难跟踪,所以某些系统提供了这些函数的另一种实现方法。每次调用这三个分配函数中的任意一个或free 时都进行附加的出错检验。在调用连接编辑程序时指定一个专用库,则在程序中就可使用这种版本的函数。此外还有公共可用的资源(例如由4.3+BSD所提供 的),在对其进行编译时使用一个特殊标志就会使附加的运行时间检查生效。
    因为存储空间分配程序的操作对某些应用程序的运行时间性能非常重要,所以某些系统提供了附加能力。例如,SVR4提供了名为mallopt的函数, 它使进程可以设置一些变量,并用它们来控制存储空间分配程序的操作。还可使用另一个名为mallinfo的函数,以对存储空间分配程序的操作进行统计。请 查看所使用系统的malloc(3)手册页,弄清楚这些功能是否可用。
    .alloca函数
    还有一个函数也值得一提,这就是alloca。其调用序列与malloc相同,但是它是在当前函数的栈帧上分配存储空间,而不是在堆中。其优点是: 当函数返回时,自动释放它所使用的栈帧,所以不必再为释放空间而费心。其缺点是:某些系统在函数已被调用后不能增加栈帧长度,于是也就不能支持 alloca函数。尽管如此,很多软件包还是使用alloca函数,也有很多系统支持它。
  8. 函数setjmp和函数longjmp
    在C中,goto语句是不能跨越函数的,而执行这种类型跳转功能函数函数setjmp和函数longjmp.
    非局部跳转语句---setjmp和longjmp函数。非局部指的是,这不是由普通C语言goto,语句在一个函数内实施的跳转,而是在栈上跳过若干调用帧,返回到当前函数调用路径上的某一个函数中。
    #include <setjmp.h>
    Int setjmp(jmp_buf  env);
       返回值:若直接调用则返回0,若从longjmp调用返回则返回非0值
    Void longjmp(jmp_buf env,int val);
    希望返回到的位置调用setjmp,此位置在main函数中,因为直接调用该函数,所以其返回值为0.setjmp参数evn的类型是一个特殊的类型jmp_buf,这一数据类型是某种形式的数组,其中存放在调用longjmp时能用来恢复栈状态的所有信息。因为需要在另一个函数中引用env变量,所以规范的处理方式是将env变量定义为全局变量。
    当检查到一个错误时,则以两个参数调用longjmp函数,第一个就是在调用setjmp时所用的env,第二个参数是具有非0值的val,它将成为从setjmp处返回的值。使用第二个参数的原因是对于一个setjmp可以有多个longjmp。
    下面我们可以看一个简单的例子:
    #include <stdio.h>
    #include <stdlib.h>
    #include <setjmp.h>
    #include <string.h>
    
    void fun1(void);
    void fun2(void);
     jmp_buf jmpbuffer;
    void main(void)
    {
        int i = 0;
        int j = 0;
        i = setjmp(jmpbuffer);//指定出错后返回的位置
        if(i==0)
        {
            printf("first run/n");
            fun1();
            fun2();
        }
       else
       {
         switch(i)
         {
           
         case 1:
           printf("In fun1 /n");
         break;
       case 2:
         printf("In fun2/n");
         break;
       default:
         printf("unkown error/n");
         break;
         }
              exit(0);
       }
        return 1;
    } 
    
    void fun1(void)
    {
        char *s = "hello";
        char *s1 = "Hello";
        if(strcmp(s,s1)!=0)
        longjmp(jmpbuffer,1);
    }
    
    void fun2(void)
    {
        char *s = "world";
        if(strcmp(s,"World")!=0)
        longjmp(jmpbuffer,2);
    }
       在使用longjmp跳转到setjmp中时,程序主动的退出了!相当于抛出一个异常退出!其实这两个函数可以模拟C++中的异常函数:
    使用setjmp和longjmp要注意以下几点:
      1、setjmp与longjmp结合使用时,它们必须有严格的先后执行顺序,也即先调用setjmp函数,之后再调用longjmp函数,以恢复到先前被保存的“程序执行点”。否则,如果在setjmp调用之前,执行longjmp函数,将导致程序的执行流变的不可预测,很容易导致程序崩溃而退出
     2、不要假设寄存器类型的变量将总会保持不变。在调用longjmp之后,通过setjmp所返回的控制流中,程序中寄存器类型的变量将不会被恢复。寄存器类型的变量,是指为了提高程序的运行效率,变量不被保存在内存中,而是直接被保存在寄存器中。寄存器类型的变量一般都是临时变量,在C语言中,通过register定义,或直接嵌入汇编代码的程序。这种类型的变量。
         longjmp必须在setjmp调用之后,而且longjmp必须在setjmp的作用域之内。具体来说,在一个函数中使用setjmp来初始化一个全局标号,然后只要该函数未曾返回,那么在其它任何地方都可以通过longjmp调用来跳转到 setjmp的下一条语句执行。实际上setjmp函数将发生调用处的局部环境保存在了一个jmp_buf的结构当中,只要主调函数中对应的内存未曾释放 (函数返回时局部内存就失效了),那么在调用longjmp的时候就可以根据已保存的jmp_buf参数恢复到setjmp的地方执行。
  9. 函数getrlimit和setrlimit
    功能描述:
    获取或设定资源使用限制。每种资源都有相关的软硬限制,软限制是内核强加给相应资源的限制值,硬限制是软限制的最大值。非授权调用进程只可以将其软限制指定为0~硬限制范围中的某个值,同时能不可逆转地降低其硬限制。授权进程可以任意改变其软硬限制。RLIM_INFINITY的值表示不对资源限制。
    #include <sys/resource.h>
    
    int getrlimit(int resource, struct rlimit *rlim);
    int setrlimit(int resource, const struct rlimit *rlim);
    参数:
    resource:可能的选择有
    RLIMIT_AS //进程的最大虚内存空间,字节为单位。
    RLIMIT_CORE //内核转存文件的最大长度。
    RLIMIT_CPU //最大允许的CPU使用时间,秒为单位。当进程达到软限制,内核将给其发送SIGXCPU信号,这一信号的默认行为是终止进程的执行。然而,可以捕捉信号,处理句柄可将控制返回给主程序。如果进程继续耗费CPU时间,核心会以每秒一次的频率给其发送SIGXCPU信号,直到达到硬限制,那时将给进程发送 SIGKILL信号终止其执行。
    RLIMIT_DATA //进程数据段的最大值。
    RLIMIT_FSIZE //进程可建立的文件的最大长度。如果进程试图超出这一限制时,核心会给其发送SIGXFSZ信号,默认情况下将终止进程的执行。
    RLIMIT_LOCKS //进程可建立的锁和租赁的最大值。
    RLIMIT_MEMLOCK //进程可锁定在内存中的最大数据量,字节为单位。
    RLIMIT_MSGQUEUE //进程可为POSIX消息队列分配的最大字节数。
    RLIMIT_NICE //进程可通过setpriority() 或 nice()调用设置的最大完美值。
    RLIMIT_NOFILE //指定比进程可打开的最大文件描述词大一的值,超出此值,将会产生EMFILE错误。
    RLIMIT_NPROC //用户可拥有的最大进程数。
    RLIMIT_RTPRIO //进程可通过sched_setscheduler 和 sched_setparam设置的最大实时优先级。
    RLIMIT_SIGPENDING //用户可拥有的最大挂起信号数。
    RLIMIT_STACK //最大的进程堆栈,以字节为单位。
    rlim:描述资源软硬限制的结构体,原型如下
    struct rlimit {
      rlim_t rlim_cur;
      rlim_t rlim_max;
    };
    返回说明:

    成功执行时,返回0。失败返回-1,errno被设为以下的某个值
    EFAULT:rlim指针指向的空间不可访问
    EINVAL:参数无效
    EPERM:增加资源限制值时,权能不允许

    更改资源规则:
    1)任何一个进程都可将一个软限制值更改为小于或等于其硬限制值
    2)任何一个进程都可降低其硬限制值,但它必须大于或等于其软限制值。这种降低,对普通用户而言是不可逆的
    3)只有超级用户进程可以提高硬限制值
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值