unix环境高级编程第7章 UNIX进程的环境.docx
《unix环境高级编程第7章 UNIX进程的环境.docx》由会员分享,可在线阅读,更多相关《unix环境高级编程第7章 UNIX进程的环境.docx(30页珍藏版)》请在冰豆网上搜索。
unix环境高级编程第7章UNIX进程的环境
发信人:
scircle (yuanyuan), 信区:
Security
标 题:
unix环境高级编程--第7章 UNIX进程的环境
发信站:
BBS 水木清华站 (Mon Mar 27 15:
53:
35 2000)
第七章〓Unix进程的环境
71〓引言
下一章将介绍进程控制原语,在此之前需先了解进程的环境。
在本章中我们将说明
:
当执行
一通程序时,其main函数是如何被调用的,命令行参数是如何传送给执行程序的;
典型的存
储器布局是什么样式;如何分配另外的存储空间;进程如何使用环境变量;进程终
止的不同
方式等。
另外,我们也将说明longjmp和setjmp函数以及它们与栈的交互作用。
在
本章结束
之前,我们将查看进程的资源限制。
72〓main函数
一道C程序总是从main函数开始执行。
main函数的原型是:
int main (int argc,char * argv[]);
其中,argc是命令行参数数,argv是指向参数的各个指针所构成的数组。
在74节
中我们将
对命令行参数进行说明。
在系统核起动一道C程序时(用一个exec函数,我们将在89节中说明exec函数),
在调用mai
n前先调用一个特殊的起动例程。
可执行程序文件将此起动例程指定为程序的起始
地址〖CD2
〗这是由连接编辑程序设置的,而连接编辑程序则由C编译程序(通常是cc)调用。
起动例程
从系统核取得命令行参数和环境变量值,然后为调用main函数作好安排。
73〓进程终止
有五种方式使进程终止:
1正常终止
(a)从main返回
(b)调用exit
(c)调用 迹茫模*常病絜xit
2异常终止
(a)调用abort(第十章)
(b)由一个信号终止(第十章)
上节提及的起动例程是这样编写的,使得从main返回后即调用exit函数。
如果将起
动例程以
C代码工表示(实际上该例程常常用汇编语言编写),则它调用main函数的形式可能
是:
exit(main(argc,argv));
exit和 迹茫模*常病絜xit函数
exit和 迹茫模*常病絜xit函数正常终止一个程序:
迹茫模*常病絜xit立即进入
系统核,
exit则先执行一些清除处理(包括的用执行各终止处P2程序,关闭所有标准I/O流
等)
#include
void exit(int status);
#include
void 迹茫模*常病絜xit (int status);
我们将在85节讨论这两个函数对其它进程,例如终止进程的父、子进程的影响。
使用不同头文件的原因是:
exit是由ANSIC说明的,而 迹茫模*常病絜xit则是由
Posix1
说明的。
由于历史原因,exit函数总是执行一个标准I/O库的清除关闭操作:
对于所有打开
流调用fcl
ose函数。
回忆55节,这造成所有在缓存中的数据都被刷新(写到文件上)。
exit和 迹茫模*常病絜xit都带一个整型参数,我们称此为终止状态。
大多数Uni
x Shell都
提供检查一个
进程终止状态的方法。
如果(a)若调用这些函数时不带终止状态,或(b)main执行了
一个无返
回值的return语句,或(c)main执行隐式返回,则该进程的终止状态是末定义的。
这就意味
着,下列经典性的C语言程序
#indude
main ()
{
〓〓printf ("hello,world \n");
}
是不完整的,因为main函数没有使用return语句返回(隐式返回),它在返回到C的
起动例程
时并没有返回一个值(终止状态)。
另外,若使用
return(0);
或者
exit(0);
则向启动执行此程序的进程(常常是一个shell进程)返回终止状态0。
另外,main函
数的说明
实际上应当是:
int main(void)
在下一章,我们针会了解到一个进程如何一道程序执行,如何等待执行该程序的进
程完成,
然后取得其终止状态。
将main说明为返回一个整型以及用exit代替return,对某些C编译程序和Unix Lin
t
(1)程序
而言会产生不必要的警告信息。
问题是这些编译程序并不了解在main中的exit与r
eturn语句
的作用相同。
警告信息可能是"control reaches end of nonvoid function。
"(控
制到达
非void函数的结束处),避开这种警告信息的一种方法是:
在main中使用return语
句而不是e
xit。
但是这样做的结果是使我们不能用Unix。
grep公用程序来找出一道程序中的所有exit调用。
另外一个解决方法是将main说明
为返回vo
id而不是int,然后的旧调用exit。
这也避开了编译程序的警告,但从程序设计角
度看却并
不正确。
在本章中,我们将main表示为返回一个整型,因为这是ANSIC和POSIX1
所定义的
。
我们将不理会编译程序不必要的警告。
atexit函数
按照ANSIC的规定,一个进程可以登记多至32个函数,这些函数将由exit自动调用
。
我们称
这些函数为终止处理程序,并用atexit函数来登记这些函数。
#include
int atexit(void (func)(void));
Returns:
0 if OK,nonzero on error返回:
若成功为0,出错为非0
其中,参数(*func)(void)是一个函数地址,当调用此函数时无需向它传送任何参
数,也不
期望它返回一个值。
exit以登记这些函数的相反顺序调用它们。
同一函数如若登记
多次,则
也被调用多次。
终止处理程序这一机制是由ANSIC新引进的。
SVR4和43+BSD都提供这种机制。
系
统V的早期
版本和43BSD则都不提供此机制。
按照ANSIC和POSIX1,exit首先调用各终止处理程序,然后按需多次调用fclose
,关闭所
有打开流。
图71摘要显示了一个C程序是如何起动的,以及它终止的各种方式。
P164
图71〓一个C程序是如何起动和终止的
注意,系统核使一道程序执行的唯一方法是调用一种exec函数。
一个进程自愿终止
的唯一方
法是显示或隐式地(调用exit)调用 迹茫模*常病絜xit。
一个进程也可非自愿地由
一个信号
使其终止(在图71中没有显示)。
实例
程序71说明了如何使用atexit函数。
执行程序71产生:
$ aout
main is done
first exit handler
first exit handler
second exit handler
注意,在main中我们没有调用exit,而是用了return语句。
P165
程序71〓终止处理程序的实例
74〓命令行参数
当执行一个程序时,调用exec的进程可将命令行参数传递给该新程序。
这是Unix
shell的一
部分常规操作。
在前几章的很多实例中,我们已经看到了这一点。
实例
程序72将其所有命令行参数都回送到标准输出上(Unix echo
(1)程序不回送第0个
参数。
)
编译此程序,并将其可执行代码文件定名为echoarg,则
$ /echoarg arg1 TEST foo
argv[0]:
/echoarg
argv[1]:
arg1
argv[2]:
TEST
argv[3]:
foo
P166
程序72〓将所有命令行参数回送到标准输出
ANSIC和POIX1都要求argv[argc]是一个空指针。
这就使我们可以将参数处理循
环改写为
:
for(i=0;arg[i]!
=NULL;i++)
75〓环境表
每个程序都接收到一张环境表。
与参数表一样,环境表也是一个字符指针数组,其
中每个指
针包含一个以null符结束的字符串的地址。
全局变量environ则包含了该指针数组
的地址。
extern char **environ;
例如:
如果该环境包含了五个字符串,那么它看起来可能如图72中所示。
P166
图72〓由五个字符串组成的环境
其中,在每个字符串的结束处都有一个null字符。
我们称environ为环境指针,指
针数组为
环境表,其中各指针指向的字符串为环境字符串。
按照惯例,环境由
name=value
这样的字符串组成,这与图72中所示相同。
大多数予定义名是完全由大写字母组
成的,但
这只是一个惯例。
在历史上,大多数Unix系统对main函数提供了第三个参数,它就是环境表地址:
int main(int argc,char *argv[],char *envp[]);
因为ANSIC规定main函数只有两个参数,而且第三个参数与全局变量environ相比也
没有带来
更多益处,所以POSIX1也规定应使用environ而不使用第三个参数。
通常用gete
nv和puten
v函数(在79节中说明)来存取特定的环境变量,而不是用environ变量。
但是,如
果要查看
整个环境,则必须使用environ指针。
76〓C程序的存储空间 季知?
nbsp;
由于历史原因,C程序一直由下列几部分组成:
·正文段。
这是由CPU执行的机器指令部分。
通常,正文段是可共享的,所以即使
是经常执
行的程序(文本编辑程序、编译程序、shell等)在存储器中也只需有一个副本。
另
外,正文
段常常是只读的,以防止一道程序由于忌外子故而修改其自身的指令。
·初始化数据段。
通常将此段就称为数据段,它包含了程序中需赋初值的变量。
例
如:
C程
序中在任何函数之外的说明:
int maxcount=99;
使此变量以初值存放在初始化数据段中。
·非初始化数据段。
通常将此段称为"bss"段,这一名称来源于早期汇编程序的一
个操作
符,意思是"block started by symbol"(由符号开始的块),在程序开始执行之前
,系统
核将此段初始化为0。
在函数外的说明
long sum[1000];
使此变量存放在非初始化数据段中。
·栈。
自动变量以及每次函数调用时所需保存的信息都存放在此段中。
每次函数调
用时,其
返回地址、以及调用者的环境信息(例如某些机器寄存器)都存放在栈中。
然后,新
被调用的
函数在栈上为其自动和临时变量分配存储空间。
以这种方式使用栈,C函数可以递
归调用。
·堆。
通常在堆中进行动态存储分配。
由于历史上形成的惯例,堆位于非初始化数
段顶和栈
底之间。
图73显示了这些段的一种典型安排方式。
这是一个程序的逻辑布局一虽然并不要
求一个具
体实现一定以这种方式安排其存储空间。
尽管如此,这给出了一个我们便于作有关
说明的一
种典型安排。
P168
图73〓典型的存储器安排
对于在VAX上的43+BSD,正文段在0号单位开始,栈顶则在Ox7fffffff之下开始
。
在VAX
机器上,在堆顶和栈底之间未用的虚地址空间是很大的。
从图73还可注意到末初始化数据段的内容并不存放在磁盘程序文件中。
需要存放
在磁盘程
序文件中的段只有正文段和初始化数据段。
size
(1)命令报告正文段、数据段和bass段的长度(单位:
字节)。
例如:
$ size/bin/cc /bin/sh
text〓data〓bss〓dec〓hex
81920〓16384〓664〓98968〓18298〓/bin/cc
90112〓16384〓0〓106496〓1a000〓/bin/sh
第4和第5列是分别以十进制和十六进制表示的总长度。
77〓共享库
现在,很多Unix系统支持共享库。
Arnold[1986]说明了一个在系统V上共享库的
一个早期
实
现,Gingell等[1987]则说明了一个在SunOs上的另一个实现。
共享库使得可
执行文件
中不再需要包含常用的库函数,而只需在所有进程都可存访的存储区中保存这种库
例程的一
个副本。
在程序第一次执行或者第一次调用每个库函数时,用动起连接方法将程序
与共享库
函数相连接。
共享库的另一个优点是可以用库函数的新版本代替老版本而无需对使
用该库的
程序重新连接编辑。
(假定参数数和类型都没有发生改变。
)
不同的系统使用了不同的方法使程序可以说明是否要使用共享库。
CC
(1)和ld
(1)命
令的选择
项是典型的。
作为长度方面发生变化的例子,下列可执行文件(典型的helloC程
序)先用无
共享库方式创建:
$ ls -1 aout
-rwxrwxr-x〓1〓stevens〓〓104859 Aug〓2 14:
25 aout
$ size aout
text〓data〓bass〓dec〓hex
49152〓49152〓0〓98304〓18000
如果我们再编译此程序使其使用共享库,则可执行文件的正文和数据段的长度都显
著减少;
$ ls -1 aout
-rwxrwxr-x〓1 stevens〓〓24576 Aug〓2 14:
26 aout
$ size aout
text〓data〓bass〓dec〓hex
8192〓8192〓0〓16384〓4000
78〓存储器分配
ANSIC说明了三个用于存储空间动态分配的函数
1 malloc。
分配指定字节数的存储区。
在此存储区中的初始值是不确定的。
2 calloc。
为指定长度的对象,分配能容纳其指定个数的存储空间。
该空间中的
每一位(b
it)都初始化为0。
3 realloc。
更改以前分配区的长度(增加或减少)。
当增加长度时,可能需将以
前分配区
的内容移到另一个足够的大区域,而新增区域内的初始值则不确定。
#include
void *malloc(size 迹茫模*常病健t—玸ize);
void *calloc(size 迹茫模*常病健t—玭obj,size 迹茫模*常病絫 size);
void *realloc(void ptr,size 迹茫模*常病絫 newsize);
All three return:
nonnull pointer if OK,NULL on error
三个函数返回:
若成功为非空指针,出错为NULL
void free(void *ptr)
这三个分配函数所返回的指针一定是适当对齐的,使其可用于任何数据对象。
例如
,在一个
特定的系统上,如果最苛刻的对齐要求是double,则对齐必须在8的倍数的地址单
元处,那
么这三个函数返回的指针都应这样对齐。
回忆我们在16节中对类属void *指针和函数原型的讨论。
因为这三个alloc函数
都返回类
属指针,如果在程序中包括了(包含了函数原型),那么当我们将这些
函数返回
的指针赋与一个不同类型的指针时,不需要作类型强制转换。
函数free释放ptr所指向的存储空间。
这种被释放的空间通常被送入可用存储区池
,以后可
在调用分配函数时再分配。
realloc使我们可以增、减以前分配区的长度(最常见的用法是增加该区。
)例如,
如果先分
配一个可容纳长度为512的数组的空间,然后在运行时填充它,然后又发现该数组
的长度还
不敷应用,则可调用realloc要求扩充该存储空间。
如果在该存储区后有足够空间
可供扩充
,则就在原存储区位置上向高地址方向扩充,并返回传送给它的同样的指针值。
如
果在原存
储区后没有足够的空间,则realloc分配另一个足够大的存储区,将现存的512个元
素数组的
内容复制到新分配到的存储区。
因为这种存储区可能会移动位置,所以不应当使用
任何指针
指在该区中。
练习418显示了在getcwd中如何使用realloc,以处理任何长度的路
程名。
程
序1527是使用realloc的另一个例子,用其可以避免使用编译时固定长度的数组
。
注意,realloc的最后一个参数是存储区的newsize(新长度),不是新、旧长度之差
。
作为
一
个特例,若ptr是一个空指针,则realloc的功能与malloc相同,分配一个指定长度
newsize
的存储区。
此功能是由ANSIC新引进的。
如果传送一个null指针,realloc的较老版本会失败。
较老版本
的这些例程允许我们realloc一块,该块是自上次malloc,realloc或calloc以来我
们所释放
的。
这种技巧可回溯到Version7,它利用了malloc实现存储器紧缩的搜索策略。
4
3+B5D仍
支持这一功能,而SVR4则不支持。
这种功能不应再使用。
这些分配例程通常是用sbrk
(2)系统调用实现的。
该系统调用扩充(或缩小)进程的
堆。
(参见
图73)。
malloc和fiee的一个样本实现请见kennighan和Ritchie[19 福福莸?
nbsp;
87节。
虽然sbrk可以扩充或缩小一个进程的存储空间,但是大多数malloc和fiee的实现都
不减小进
程的存储空间。
释放的空间可供以后再分配,但将它们保持在malloc池中而不返回
给系统核
。
应当了解的是,大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间
用来记录
管理信息〖CD2〗分配块的长度,指向下一个分配块的指针等等。
这就意味着如果
写过了一
个已分配区的尾端,则会改写后一块的管理信息。
这种类型的错误是灾难性的,但
是因为这
种错误小会很快就表露出来,所以也就很难发现。
将指向分配块的指针向后移动也
可能会改写本块的管理信息。
其它可能产生的致命性的错误是:
释放一个已经释放了的块;调用fiee时所用的指
针不是三个alloc函数的返回值等。
因为存储器分配出错很难跟踪,所以某些系统提供了这些函数的另一种实现方法
。
每次调用
这三个分配函数中的任一个或fiee时都进行附加的出错检验。
在调用连接编辑程序
时指定一
个专用库,则在程序中就可使用这种版本的函数。
也有公共可用的资源(例如由4
3+BSD所
提供的),在对其进行编译时使用一个特殊标志就会使附加的运行时检查生效。
因为存储空间分配程序的操作对某些应用程序的运行时性能是非常重要的,所以某
些系统提
供了附加能力。
例如,SVR4提供了名为mallop函数,它使一个进程可以设置一些变
量,用它
们来控制存储空间分配程序的操作。
还可使用另一个名为mallinfo的函数,以对存
储空间分
配程序的操作进行统计。
请查看你所使用系统的malloc(3)手册实,弄清楚这些功
能是否可
用。
alloca函数
还有一个函数也值得一提,这就是alloca。
其调用序列与malloc相同,但是它是在
当前函数
的栈帧上分配存储空间,而不是在堆中。
其优点是:
当函数返回时,自动释放它所
使用的栈
帧,所以我们就不必再为释放空间而费心。
其缺点是某些系统在函数已被调用后不
能增加栈
帧长度,于是也就不能支持alloca函数。
尽管如此,很多软件包函是使用alloca函
数,也有
很多系统支持它。
79〓环境变量
如同前述,环境字符串的形式是:
name=vulue
Unix系统核并不关心这种字符串的意义,它们的解释完全取决于各个应用程序。
例
如,Shel
l使用了大量的环境变量。
其中某一些在登录时自动设置(HOME,USER等),有些则
由用户设
置。
我们通常在一个shell起动文件中设置环境变量以控制shell的动作。
例如,若
我们设置
了环境变量MAILPATH,则它就告诉了Bourne shell和Korn shell到那里去查看邮件
。
ANSIC定义了一个函数getenv,可以用其取环境变量值,但是该标准又称环境的内
容是实现
定义的。
#include
char *getenv(const char *name);
Returns:
pointer to value associated with name,NULL if not found
返回:
指向与name关联的value的指针,若未找到则为NULL
注意,此函数返回一个指针,它指向name=value字符串中的value。
我们总是应当
使用geter
v从环境中取一个环境变量的值,而不是直接存取environ。
POSIX1和XPG3定义了某些环境变量。
图74列出了由这二个标准定义并受到SVR
4和43+B
SD支持的环境变量。
SVR4和43+BSD还使用了很多依赖于实现的环境变量。
FIPS 151-1要求一个登录shell一定要定义环境变量HOME和LOGNAME。
P172
图74〓环境变量
除了取环境变量值,有时也需要设置环境变量或者是改变现有变量的值,或者是增
加新的环
境变量。
(在下一章将会了解到,我们能影响的是当前进程及其后生成的子进程的
环境,我
们不能影响父进程的环境,这通常是一个shell进程。
尽管如此,修改环境表的能
力仍然是
很有用的。
)不幸的是,并不是所有系统都支持这种能力。
图75列出了由不同的
标准及实
现支持的各种函数。
P173
图75〓对于各种环境表函数的支持
POSIX1标准说明puterv和clearenv正被考虑加到POSIX1的修订版中。
在图75中,中间三个函数的原型是:
#include
int putenv(const char *str);
int setenv(const char name,const char value,int rewrite);
Both return:
0 if OK,nonzero on error两个函数返回:
若成功为0,出错为