第七章 进程环境
7.1 引言
本章将学习:程序执行时,main函数时如何让被调用的;命令行参数如何传递给新程序的;典型的存储空间布局是什么样式;如何分配另外的存储空间;进程如何使用环境变量;进程的各种不同终结方式等。另外还将说明longjmp和setjmp函数以及它们与栈的交互作用。
7.2 main函数
当内核执行main函数前,会调用一个特殊的启动例程。连接编辑器设定该启动例程为程序的起始地址。连接编辑器则由C编译器调用。
7.3 进程终止
有8种不同的方式使的进程终止,其中5种为正常终止方式
- 从main返回;
- 调用exit;
- 调用_exit或_Exit;
- 最后一个线程从启动例程返回;
- 从最后一个线程调用pthread_exit
三种异常终止方式
- 调用abort
- 接到一个信号
- 最后一个线程对取消请求作出相应
退出函数
三个函数用于正常终止一个程序:_exit和_Exit立刻进入内核,exit则先执行一些清理处理,然后返回内核。
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);
exit函数总是执行一个标准I/O库的清洗关闭操作:对于所有打开流调用fclose函数。这造成输出缓冲中的所有数据都被冲洗(写到文件上)。
以上三个函数都带有一个整型参数,称为终止状态,如果程序处于以下三种状态中的某一种,则该进程的终止状态是未定义的。
- 调用这些函数时,不带有终止状态。
- main执行了一个无返回值的return语句。
- main没有声明返回类型为整型。
函数 atexit
一个进程可以登记多至32个函数,这些函数由exit自动调用,被称为终止处理程序,atecit就是用来登记这些函数的。
#include <stdlib.h>
int atexit(void (*func)(void));
//返回值:若成功,返回0;若出错,返回非0
相当于将函数放到一个栈中,exit调用这些函数的顺序,与登记时的顺序相反,同一函数如果登记多次也会被调用多次。
C程序的启动过程:
7.4 命令行参数
ISO C和POSIX.1都要求argv[argc]是一个空指针。我们的参数处理循环可以改写为NULL终止:
for(int i = 0; argv[i] != NULL; i++);
7.5 环境表
每个程序都接收到一张环境表。与参数表一样,环境表也是字符指针数组,其中每个指针包含一个以null结束的C字符串的地址。全局变量environ则包含了该指针数组的地址:
extern char **environ;
7.6 C程序的存储空间布局
C程序一直由下列几部分组成:
- 正文段:由CPU执行的机器指令部分。通常正文段可以共享。
- 初始化数据段:包含了程序中明确赋初值的变量。
- 未初始化数据段:通常将此段称为bass段,在程序开始执行之前,内核将此段中的数据初始化为0或者空指针。
- 栈:自动变量以及函数调用时所需要保存的信息存放于此。
- 堆:进行动态存储分配。
上图是程序的逻辑布局,是一种典型安排,并不要求一个具体实现一定以这种方式安排其存储空间。
7.7 共享库
共享库使得可执行文件中不再需要包含公用的库函数,而只需在所有进程都可引用的存储区中保存这种库例程的一个副本。这减少了每个可执行文件的长度,但增加了一些运行时间开销。
共享库的另一个优点是可以用库函数的新版本代替老版本而无需对使用该库的程序重新连接编辑。
7.8 存储空间分配
三个用于存储空间分配的函数:
- malloc:分配指定字符数的存储区(此函数中的初始值不确定)
- calloc:为指定数量,指定长度的对象分配存储空间。(该空间中的每一位都初始化为0)
- realloc:增加或减少以前分配区的长度。增加长度时,可能需要将以前分配区的内容转移到另一个足够大的区域,以便在尾端提供增加的存储区。(新增的区域内的初始值不确定)
#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(vpid *ptr);//释放空间
- 这三个分配函数所返回的指针一定是适当对齐的,使其可用于任何数据对象。
- 这三个分配函数都返回通用指针 void *。
- 函数free时方ptr指向的存储空间。
- 这些分配例程通常用sbrk(2)系统调用实现。该系统调用扩充或缩小进程的堆。
- 虽然sbrk可以扩充或缩小进程的存储空间,但大多数malloc和free的实现都不减小进程的存储空间。释放的空间可供以后再分配,但将他们保持在malloc池中,而不返回给内核。
- 大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记录管理信息——分配块的长度、指向下一个分配块的指针等。
可能发生的致命性错误:
- 在动态分配的缓冲区前或后进行写操作。
- 释放一个已经释放的块。
- 调用free时所用的指针,不是三个alloc函数的返回值
7.9 环境变量
UNIX内核并不查看这些字符串,它们的解释完全取决于各个应用程序。
函数getenv:获取环境变量值。
#include <stdlib.h>
char *getenv(const char *name);
//返回值:指向与name关联的alue指针;若未找到,返回NULL
此函数返回一个指针,它指向 name=value中的value
不同系统对于各种环境表函数的支持:
中间三个函数的原型是:
#include <stdlib.h>
int putenv(char *str);
//函数返回值:若成功,返回0;若出错,返回非0
int setenv(const char *name, const char *value, int rewrive);
int unsetenv(const char *name);
//返回值:若成功,返回0;若出错,返回-1
这三个函数的操作如下:
- putenv取形式为name=value的字符串,将其放到环境表中。如果name已经存在,则县删除其原来的定义。
- setenv将name设置为value。如果环境中name已经存在,那么若rewrite非0,则首先删除其现有的定义;若rewrite为0,则不删除其现有的定义(name不会更新为value,且不会返回错误)。
- unsetenv删除name的定义。即使不存在这种定义也不会出错。
7.10 函数setjmp和longjmp
正确的使用方式就是尽量不使用它们。
在C中,goto语句是不能跨越函数的,执行这种类型跳转功能的函数是setjmp和longjmp
7.11 函数getrlimit和setrlimit
每个进程都有一组限制资源,其中一些可以用getrlimit和setrlimit函数查询和更改。
#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlptr);
int setrlimit(itn resource, const struct rlimit *rlptr);
//两个函数返回值:若成功,返回0;若出错,返回非0
在更改资源时,必须遵循下列三条规则
- 任何一个进程都可以将一个软限制值更改为小于或等于其硬限制值。
- 任何一个进程都可以降低其硬限制值,但是它必须大于或等于其软限制值。这种降低,对普通用户来说是不可逆的。
- 只有超级用户可以提高硬限制值。