首页 > 代码库 > APUE学习笔记:第七章 进程环境

APUE学习笔记:第七章 进程环境

7.1 引言

本章将学习:当执行程序时,其main函数是如何被调用的;命令行参数是如何传送给执行程序的;典型的存储器布局是什么样式;如何分配另外的存储空间;进程如何使用环境变量;各种不同的进程终止方式等;另外还将说明longjmp和setjmp函数以及它们与栈的交互作用;还将介绍研究进程的资源限制

 

7.2 main函数

C程序总是从main函数开始执行。当内核执行C程序时,在调用main前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址——这是由连接编辑器设置的,而连接编辑器则由C编译器调用。启动例程从内核取得命令行参数和环境变量值,然后为按上述方式调用main函数做好安排

 

7.3 进程终止

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

(1)从main返回

(2)调用exit

(3)调用_exit或_Exit

(4)最后一个线程从其启动例程返回

(5)最后一个线程调用pthread_exit

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

(6)调用abort

(7)接到一个信号并终止

(8)最后一个线程对取消请求做出响应

上一节的启动例程是这样编写的,使得从main返回后立即调用exit函数:exit(main(argc,argv));

 

1.exit函数

有三个函数用于正常终止一个程序:_exit和_Exit立即进入内核,exit则先执行一些清理处理(包括调用执行各终止处理程序,关闭所有标准I/O流等),然后进入内核

#include<stdlib.h>    //ISOvoid exit(int status);void _Exit(int status);#include<unistd.h>    //POSIX.1void _exit(int status);

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

三个函数都带一个整型参数,称之为终止状态。大多数UNIX shell都提供检查进程终止状态的方法。如果(a)若调用这些函数时不带终止状态,或(b)main执行了一个无返回值的return语句,或(c)main没有声明返回类型为整型,则该进程的终止状态是未定的。

2.atexit函数

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

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

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

内核使程序执行的唯一方法是调用一个exec函数

实例:7_2 终止处理程序实例

 1 #include"apue.h" 2  3 static void my_exit1(void); 4 static void my_exit2(void); 5  6 int main() 7 { 8     if(atexit(my_exit2)!=0) 9         err_sys("can‘t register my_exit2");10     if(atexit(my_exit1)!=0)11         err_sys("can‘t register my_exit1");12     if(atexit(my_exit1)!=0)13         err_sys("can‘t register my_exit1");14     15     printf("main is done\n");16     return(0);17 }18 static void my_exit1(void)19 {20     printf("first exit handler\n");21 }22 static void my_exit2(void)23 {24     printf("second exit handler\n");25 }

执行后可以看出:终止处理程序每登记一次,就会被调用一次,且exit调用这些函数的顺序与它们登记时候的顺序相反。

 

7.4 命令行参数

当执行一个程序时,调用exec的进程可将命令行参数传递给该新进程。

实例:7_3 将所有命令行参数回送到标准输出

#include"apue.h"int main(int argc,char *argv[]){    int i;    for(i=0;i<argv;i++)    printf("argv[%d]:%s\n",i,argv[i]);    exit(0);}

 

7.5 环境表

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

 

7.6 C程序的存储空间布局

从历史上讲,C程序一直由下面几部分组成:

-正文段。这是由cpu执行的机器指令部分。通常,正文段是可共享的。所以即使频繁执行的程序(如文本编辑器,C编译器和shell等)在存储器中也只需有一个副本,另外,正文段常常是只读的,以防止程序由于意外而修改其自身指令

-初始化数据段。通常将此段称为数据段,它包含了程序中需明确地赋初值的变量。例如,C程序中出现在任何函数之外的声明:int maxcount=99,使此变量带有其初值存放在初始化数据段中

-非初始化数据段。通常将此段称为bss段,这一名称来源于一个早期的汇编运算符,意思是“block started by symblo”(由符号开始的块),在程序开始执行之前,内核将此段中的数据初始化为0或空指针。出现在任何函数外的C声明:long sum[1000];使此变量存放在非初始化数据段中

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

-堆。通常在队中进行动态存储分配。由于历史形成的惯例,堆位于非初始化数据段和栈之间

 

size(1)命令报告正文段、数据段和bss段的长度

 

7.7 共享库

共享库使得可执行文件不在需要包含公用的库例程,而只需在所有进程都可引用的存储区中维护这种库例程的一个副本。程序第一次执行或者第一次调用某个库函数时,用动态链接方法将程序与共享库函数相链接。这减少了每个可执行文件的长度,但增加了一些运行时间的开销。这种时间开销发生在该程序第一次被执行时或者每个共享库函数第一次被调用时。共享库的另一个有点是可以用库函数的新版本代替老版本,而无需对使用该库的程序重新链接编辑

 

7.8 存储器分配

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

(1)malloc :分配指定字节数的存储区。此存储区中的初始值不确定。

(2)calloc  :为指定数量具指定长度的对象分配存储空间。该控件中的每一位都初始化为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);                //三个函数返回值:若成功则返回非空指针,若出错则返回NULLvoid free(void *ptr);

这三个分配函数多返回的指针一定是适当对齐的,使其可用于任何数据对象。

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

 

7.9 环境变量

ISO C定义了一个函数getenv,可以用其取环境变量值,但是该标准又称环境的内容是由实现定义的。

#include<stdlib.h>char *getenv(const char *name);            //返回值:指向与name关联的value的指针,若未找到则返回NULL

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

 

#include<stdlib,h>int putenv(char *str);int setenv(const char *name,const char *value,int rewrite);int unsetenv(const char *name);                //三个函数返回值:若成功则返回0,若出错则返回非0值

这三个函数的操作是:

-putenv取形式为name=value的字符串,将其放到环境表中。如果name已经存在,则先删除其原理的定义。

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

-unsetenv删除name的定义。即使不存在这种定义也不出错

 

如果修改一个现有的name:

a.如果新value的长度少于或等于现有value的长度,则只要在原字符串所用空间中写入新字符串

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

 

如果要增加一个新的name:

a.如果这是第一次增加一个新name,则必须调用malloc为新的指针表分配空间。接着,将原来的环境表复制到新分配区,并将指向新name=value字符串的指针存放在该指针表的表尾,然后又将一个空指针存放在其后。最后使environ指向新指针表。

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

 

7.10 setjmp和longjmp函数

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

 

实例:7_4  进行命令处理的典型程序骨架

 1 #include"apue.h" 2  3 #define TOK_ADD 5 4  5 void do_line(char *); 6 void cmd_add(void); 7 int get_token(void); 8  9 int main()10 {11     char line[MAXLINE];12     while(fgets(line,MAXLINE,stdin)!=null)13         do_line(line);14     exit(0);15 }16 char *tok_ptr;  //global pointer for get_token()17 18 void do_line(char *ptr)    //process one line of input19 {20     int cmd;21     tok_ptr=ptr;22     while((cmd=get_token())>0){23     switch(cmd){//one case for each command24     case TOK_ADD:25         cmd_add();26         break;27     }28     }29 }30 void cmd_add(void)31 {32     int token;33     token=get_token();34     //rest of processing for this command35 }36 int get_token(void)37 {38     //fetch next token from line pointed to by tok_ptr39 }

其主循环是从标准输入读一行,然后调用do_line处理该输入行。do_line函数调用get_token从该输入行中取下一个标记。一行中的第一个标记假定是一条某种形式的命令,于是switch语句就实现命令选择。

 

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

在希望返回到的位置调用setjmp.setjmp参数env的类型是一个特殊类型jmp_buf。这一数据类型是某种形式的数组,其中存放在调用longjmp时能用来恢复栈状态的所有信息。因为需要另一个函数中引用env变量,所以规范的处理方式是将env变量定义为全局变量

 

实例:7_5 setjmp和longjmp实例

http://blog.163.com/muren20062094@yeah/blog/static/1618444162011529103634600/

可直接参考上述链接,总的来说,就是setjmp设置跳转的位置,longjmp跳到那个位置,但跳转只是回来原来的位置,并不实现回滚自动变量和寄存器的值(在上面代码中加上一个静态全局变量,不断自增输出,即可看出)

 

 

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

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

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

在更改资源限制时,须遵循下列三条规则:

(1)任何一个进程都可将一个软限制更改为小于或等于其硬限制值

(2)任何一个进程都可降低其硬限制值,但它必须大于或等于其软限制值。这种降低对普通用户而言是不可逆的

(3)只有超级用户进程可以提高硬限制值

常量RLIM_INFINITY指定了一个无限量的限制

 

实例:7_8 打印当前资源限制

 1 #include"apue.h" 2 #if defined(BSD)||defined(MACOS) 3 #include<sys/time.h> 4 #define FMT "%10lld " 5 #else 6 #define FMT "%10ld " 7 #endif 8 #include<sys/resource.h> 9 10 #define doit(name) pr_limits(#name,name)11 static void pr_limits(char *,int);12 int main(void)13 {14     #ifdef RLIMIT_AS15     doit(RLIMIT_AS);16     #endif17     doit(RLIMIT_CORE);18     doit(RLIMIT_CPU);19     doit(RLIMIT_DATA);20     doit(RLIMIT_FSIZE);21     #ifdef RLIMIT_LOCK22     doit(RLIMIT_LOCKS);23     #endif24     #ifdef    RLIMIT_MEMLOCK25     doit(RLIMIT_MEMLOCK);26     #endif27     doit(RLIMIT_NOFILE);28     #ifdef RLIMIT_NPROC29     doit(RLIMIT_NPROC);30     #endif31     #ifdef RLIMIT_RSS32     doit(RLIMIT_RSS);33     #endif34     #ifdef RLIMIT_SBSIZE35     doit(RLIMIT_SBSIZE);36     #endif37     doit(RLIMIT_STACK);38     #ifdef RLIMIT_VMEM39     doit(RLIMIT_VMEM);40     #endif41     exit(0);42 }43 static void pr_limits(char *name,int resource)44 {45     struct rlimit limit;46     if(getrlimit(resource,&limit)<0)47     err_sys("getrlimit error for %s",name);48     printf("%-14s ", name);49     if(limit.rlim_cur==RLIM_INFINITY)50     printf("(infinite) ");51     else52     printf(FMT,limit.rlim_cur);53     if(limit.rlim_max==RLIM_INFINITY)54     printf("(infinite)");55     else56     printf(FMT,limit.rlim_max);57     putchar((int)\n);58 }