[APUE]UNIX进程的环境(下)

一、共享库

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

不同的系统使用不同的方法说明程序是否需要使用共享库。比较典型的有cc和ld命令的可选项。

二、 存储器分配

ANSI C说明了三个存储空间动态分配的函数
(1) malloc。分配指定字节数的存储区。此存储区中的初始值不确定。 (2) calloc。在内存中动态地分配nobj个长度为size的连续空间。该空间中的每一位都初始化为0。 (3) realloc。更改以前分配区的长度(增加或减少)。当增加长度时,可能需要将以前分配区的内容移到另一个足够大的区域,而且新增区域内的初始值不确定。

#include <stdlib.h>

void *malloc(size_t size);
void *calloc(size_t nboj, size_t size);
void *realloc(void *ptr, size_t newsize);
三个函数返回:成功返回为非空指针,出错为NULL
void free(void *ptr);

这三个分配函数返回的指针一定是适当对齐的,使其可以用于任何数据对象。在一个特定的系统上,如果最苛刻的对齐要求是double,则对齐必须在8的倍数的地址单元处,那么这三个函数返回的指针都应这样对齐。   free函数释放的空间通常被送入可用存储区池,以后可在调用分配函数时再调用。   realloc如果在原存储区后有足够的空间可供扩充,则可在原存储区位置上向高地址方向扩充。并返回传给它的同样的指针值。如果在原存储区后没有足够的空间则realloc分配一个足够大的存储区,将现存的内容复制到新分配的存储区中。因为这种存储区会移动位置所以不应使任何指针指到该区。   realloc的最后一个参数是存储区的newsize而不是新旧长度之差。如果ptr是空指针,则realloc功能与malloc相同。用于分配一个制定长度newsize的存储区。
  这些分配例程通常通过sbrk系统调用实现。该系统调用扩充或缩小进程的堆。
  虽然sbrk可以扩充或缩小一个进程的存储空间,但是大多数malloc和free的实现都不减小进程的存储空间而是将它们保存在malloc池中而不返回给内核。
  大多数实现所分配的存储空间比所要求的要大,额外的空间用来记录管理信息--分配块的长度,指向下一个分配块的指针等等。这就意味着如果写过一个已分配区的尾端,则会改写后一块的管理信息。将指向分配块的指针向后移动可能也会改写本块的管理信息。
  其他可能出现的错误:释放一个已经释放了的块;调用free所用的指针不是三个alloc函数的返回值等。

alloca函数

alloca函数是在当前函数的栈帧上分配存储空间。优点是:当函数返回时自动释放它所使用的栈帧,缺点是:某些系统在函数已经被调用后不能增加栈帧长度,于是也就不能支持alloca函数。

三、环境变量

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

#include <stdlib.h>

char *getenv(const char *name);
返回值:指向与name关联的value的指针,未找到则返回NULL

POSIX.1和XPG3定义了某些环境变量。下表列出了由这两个标准定义并受到SVR4和4.3+BSD支持的环境变量。

[APUE]UNIX进程的环境(下)

除了取环境变量值,有时也需要设置环境变量,或者是改变现有变量的值,或者是增加新的环境变量。但是不是所有系统都支持这些操作。下表列出了不同的标准及实现支持的各种函数:

[APUE]UNIX进程的环境(下)

中间三个函数的原型是:

#include <stdlib.h>

int putenv(const char *str);
int setenv(const char *name, const char *value, int rewrite);
两个函数返回:成功为0,失败非0.
void unsetenv(const char *name);

这三个函数的操作是:

  • putenv取形式为name=value的字符串,将其放到环境表中。如果name已存在则覆盖之前的定义。
  • setenv将name设置为value。如果name已存在,(a)如果rewrite非0,则覆盖.(b)如果rewrite为0,则不覆盖而且也不出错。
  • unsetenv删除name的定义,即使name不存在也不出错。
    **这些函数在修改环境表时是如何进行操作的呢?**上一节中内存分配的那张图中,环境表和环境字符串典型的存放在进程存储空间的顶部(栈之上)。删除一个字符串很简单--只要找到该指针,然后将所有后续指针都向下移一个位置。但是增加一个字符串或修改一个现存的字符串就比较困难。栈以上的空间因为已处于进程存储空间的顶部所以无法扩充,即无法向上扩充也无法向下扩充。
    (1) 如果修改一个现存的name:
      (a) 如果新value的长度少于或等于value的长度,则只要在原字符串所用空间中写入新字符串。
      (b) 如果新value的长度大于原长度,则必须调用malloc为新字符串分配空间,然后将新字符写入该空间中,然后使环境表中针对name的指针指向新分配区。
    (2) 如果要增加一个新的name,则操作更为复杂。首先调用malloc为name=value分配空间然后将该字符串写入该空间。然后:
      (a) 如果这是第一次增加一个新name,则必须调用malloc为新的指针表分配空间。将原来的环境表复制到新分配区。并将指向新name=value的指针存在该指针表的表尾,然后又将一个空指针存在其后。最后使environ指针指向新指针表。再看上一节中的内存分配图,如果原来的环境表位于栈顶之上(这是常见情况)那么必须将此表移至堆中。但是此表中的大多数指针仍指向栈顶之上的个name=value字符串。
      (b) 如果这不是第一次增加一个新name,则可知以前调用malloc在堆中为环境表分匹配了空间,所以只要调用realloc,以分配比原来空间多一个指针的空间。然后将该指向新name=value字符串的指针存放在该表表尾,后面跟着一个空指针。

四、setjpm和longjmp函数

在C中不允许使用跳跃函数的goto语句。而执行这种跳转功能的是非局部跳转函数setjmp和longjmp。非局部表示这不是子啊一个函数内的普通的C语言goto语句,而是在栈上跳过若干调用栈,返回到当前函数调用路径上的一个函数中。

#include <setjmp.h>

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

在希望返回到的位置调用setjmp,因为我们直接调用该函数所以其返回值为0。setjmp的参数env是一个特殊类型jmp_buf。这一数据类型是某种形式的数组,其中存放在调用longjmp时能用恢复栈状态的所有信息。一般,env变量是个全局变量,因为需要从另一个函数中引用它。
  当检查到一个错误时,则调用longjmp函数,第一个参数就是在调用setjmp时所用的env,第二个val是个非0值,它成为从setjmp处返回的值。使用第二个参数的原因是对于一个setjmp可以有多个longjmp。

下面是APUE上使用setjmp/longjmp的实例

[APUE]UNIX进程的环境(下)

执行main时,调用setjmp,它将所需的信息记入变量jmpbuffer中返回0。然后调用do_line,它又调用cm_add,假定在其中检测到一个错误。在cmd_add中调用longjmp之前,栈的形式如图所示
  [APUE]UNIX进程的环境(下)

但是longjmp使栈回到执行main函数时的情况,也就是抛弃了cmd_add和do_line的栈帧。调用longjmp造成main中setjmp的返回。但是,这一次的返回值是1(longjmp的第二个参数)。

1. 自动、寄存器和易失变量

在main函数中,自动变量和寄存器变量的状态如何?当longjmp返回到main函数时,这些变量的值是否能恢复到以前调用setjmp时的值(即滚回原先值),或者这些变量的值保持为调用do_line时的值(do_line调用cmd_add,cmd_add又调用longjmp)?大多数实现并不滚回这些自动变量和寄存器变量的值,而所有标准则说它们的值是不确定的。如果有一个自动变量而又不想使其数值滚回可以定义其为具有volatile属性。说明为全局和静态变量的值在执行longjmp时保持不变。   我们通过以下程序来说明在调用longjmp后,自动变量、寄存器变量和易失变量的不同情况。

#include <setjmp.h>

static void f1(int, int, int);
static void f2(void); static jmp_buf jmpbuffer; int main(void)
{
int count;
register int val;
volatile int sum; count ; val = ; sum = ;
if (setjmp(jmpbuffer) != ) {
printf("after longjmp: count = %d, val = %d, sum = %d\n", count, val, sum);
exit();
} count = ; val = ; sum = ;
f1(count, val, sum);
} static void f1(int i, int j, int k)
{
printf("in f1():count = %d, val = %d, sum = %d\n", i, j, k);
f2();
} static void f2(void)
{
longjmp(jmpbuffer, );
}

如果以不带优化和带优化对此程序分别进行编译,然后运行它们得到的结果是不同的:
  [APUE]UNIX进程的环境(下)

易失变量不受优化的影响,在longjmp之后的值,是它在调用f1时的值。存放在存储器中的变量将具有longjmp时的值,而在CPU和浮点寄存器中的变量则恢复为调用setjmp时的值。不进行优化时所有这三个变量都存放在存储器中(会忽略val寄存器存储优化)。而进行优化时,count和val都存放在寄存器中。sum由于加了volatile限定符(该限定符修饰表示告诉编译器不要对这个变量进行优化)所以不会放到寄存器中。

2. 自动变量的潜在问题

对于如下程序,有一个open_data的函数,它打开了一个标准IO流,然后为该流设置缓存:

#include <stdio.h>
#define DATAFILE "datafile" FILE *open_data(void)
{
FILE *fp;
char databuf[BUFSIZ]; /* setvbuf设置的标准IO缓存 */ if ((fp = fopen(DATAFILE, "r")) == NULL) {
return (NULL);
}
if (setvbuf(fp, databuf, _IOLBF, BUFSIZ) != 0) {
return (NULL);
}
return (fp);
}

该程序存在的问题是:当open_data返回时,它在栈上所使用的空间将由下一个被调用函数的栈帧使用。但是,标准IO函数仍然使用原先在栈上分配的存储空间作为流的缓存。这就产生了问题。为了改正这个问题应该在全局空间静态的(如static或extern),或者动态的为数组分配空间(malloc在堆上分配)。

五、getrlimit和settlimit函数

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

#include <sys/time.h>
#inlcude <sys/resource.h> int getrlimit(int resource, struct rlimit *rlptr);
int setrlimit(int resource, const struct rlimit *rlptr);
返回值:成功为0,出错非0.

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

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

这两个函数不属于POSIX.1,但SVR4和4.3+BSD提供
SVR4在上面的结构中使用基本系统数据类型rlim_t,其他系统则将这两个成员定义为整型或长整型。
进程的资源限制通常是在系统初始化时由0进程建立的,然后由后续进程继承。在SVR4中,系统默认值可以查看文件/etc/conf/cf.d/mtune。在4.3+BSD中,系统默认值分散在多个头文件中。

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

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

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

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

一个无限量的限制通常由常数RLIM_INFINITY指定。

这两个函数的resource参数取下列值之一。并非所有资源限制都受到SVR4和4.3+BSD的支持。

  • RLIMIT_CODE (SVR4及4.3+BSD) core文件的最大字节数,若其值为0则组织创建core文件。
  • RLIMIT_CPU(SVR4及4.3+BSD) CPU时间的最大量值(秒),当超过此限制时,向该进程发送SIGXCPU信号。
  • RLIMIT_DATA(SVR4及4.3+BSD) 数据段的最大字节长度。这是图中初始化数据、非初始化数据以及堆的总和。
  • RLIMIT_FSIZE (SVR4及4.3+BSD) 可以创建的文件的最大字节长度。当超过此软限制时,则向该进程发送SIGXFSZ信号。
  • RLIMIT_MEMLOCK (4.3+BSD) 锁定在存储器地址空间(尚未实现)。
  • RLIMIT_NOFILE (SVR4) 每个进程能打开的最多文件数。更改此限制将影响到sysconf函数在参数_SO_OPEN_MAX中返回的值。
  • RLIMIT_NPROC (4.3+BSD) 每个实际用户ID所拥有的最大子进程数。更改此限制将影响到sysconf函数在参数_SC_CHILD_MAX中返回的值。
  • RLIMIT_OFILE (4.3+BSD) 与SVR4的RLIMIT_NOFILE相同
  • RLIMIT_RSS (4.3+BSD) 最大驻内存集字节长度(RSS)。如果物理存储器供不应求则内核将从进程处取回超过RSS的部分。
  • RLIMIT_STACK (SVR4及4.3+BSD)栈的最大字节长度。
  • RLIMIT_VMEN (SVR4) 可映照地址空间的最大字节长度。这影响到mmap函数。

资源限制将影响到调用进程并由其子进程继承。这就意味着为了影响一个用户的所有后续进程,需将资源限制设置构造在shell中。

上一篇:07.深入浅出 Spring Boot - 数据访问之Mybatis(附代码下载)


下一篇:Unix系统中system函数的返回值