APUE第7章 进程环境


进程的终止

退出函数

PS:前两个的头文件为<stdlib.h>,后面的头文件为<unistd.h>

函数原型

  1. void exit(int status)
  2. void _Exit(int status)
  3. void _exit(int status)

函数联系与区别

  1. 在main函数中的return和exit是等价的
  2. 进程资源终止的唯一方法是显示地或隐式地(通过调用exit)调用_exit或_Exit。
  3. exit和_exit、_Exit的区别在于exit函数会调用终止处理程序(atexit注册的终止处理程序)和检查打开的文件并且调用用户空间的标准I/O清理程序(如fclose),而_exit和_Exit两个函数不进行这些处理,直接返回到内核处理。

atexit函数

atexit的声明如下

int atexit(void (*func)(void));

传入的函数要求返回值和参数均为空,同一个函数被登记多次,也一样会执行多次,执行顺序和登记顺序相反。

命令行参数的传递

标准C/C++程序的主函数如下

int main(int argc, char *argv[]){
    return 0;
}

其中argc表示参数个数,argv是char*数组,ISO C和POSIX.1都要求argv[argc]是一个空指针

环境表

环境表的声明为

extern char **environ;

即一个char*数组,每一个char*都以’\0’结尾,字符串的内容为”NAME=VALUE”格式,如”HOME=/home/sar\0”

一般获取和设置某个环境变量是通过getenv()和putenv()函数实现的,但是要查看整个环境就只能访问environ指针

存储空间分布

之前面试好像被问过这道题
从低地往高地址分别是

  1. 正文段

    正文段是CPU执行的机器指令部分,一般可共享(使得代码副本只有一份)且只读(保证指令不被修改),由exec从程序文件读入
  2. 初始化的数据段

    程序中显示赋值初始化了的数据存储位置,由exec从程序文件读入
  3. 未初始化的数据段(bss即block started by symbol)

    未赋值的数组和指针,一般由exec初始化为0或空指针


  4. 堆的增长方向是向高地址的方向增长,堆上一般是提供动态分配的变量存储。


  5. 栈的增长方向是往低地址的方向增长,栈上一般存储的是函数调用需要保存的信息,如:返回地址,调用者的环境信息,临时变量。
  6. 命令行参数和环境变量
    这个存放在存储空间最高地址的地方

共享库

大多数Unix系统都支持共享库。

作用

  1. 使得可执行文件中不再需要包含公用的库函数,只需引用存储区中的副本。
  2. 在第一次调用该库函数的时候动态链接程序和共享库函数,能减少执行文件的长度,但增加了第一次链接时的时间开销。
  3. 共享库的新版本代替老版本的时候不需要重新对该程序进行重新连接

存储空间的分配

ISO C有三个用于空间动态分配的函数

  1. malloc,分配指定字节的存储区。此存储区中的初始值不确定
  2. calloc,为指定数量指定长度的对象分配存储空间,该空间的每一位(bit)都初始化为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);

这三个函数所返回的指针一定都是适当对齐的,使其可以用于任何数据对象。书上有这句原话:

如果最苛刻的对齐要求是,double必须在8的倍数地址单元处开始,那么这三个函数返回的指针都应该这样对齐

realloc的细节

  1. realloc传入的第二个参数是newsize,而不是增加多少或减少多少的值,因为size_t一般底层是unsigned int,所以是不可能有负值的
  2. 在扩充空间的时候,原来的位置可能无法扩充到那么大,则会重新开辟一块新的空间,并将原来的内容复制到新的位置,再释放原来的空间。所以不应该使用任何指针指在该区中
  3. 当realloc的第一个参数为空指针的时候,realloc和malloc的作用相同

其他应该注意的细节

  1. 大多数malloc等的实现所真实分配的空间会比申请的大一些,多出来的空间用于记录分配块的长度、指向下一个分配快的指针等。所以如果在读写的时候发生越界,有可能就会抹去或者改写了这些管理信息或者改写了其他动态分配变量的内容,这种错误是灾难性的,而且不太容易确定源头,但不一定很快就暴露出来
  2. double free问题,如果指针已经free过了,再free会引发致命错误;如果free的参数不是上面三个所提到的函数返回的指针,也会引发问题
  3. leak问题,如果分配了空间不free的话,进程空间长度会慢慢增加,直到没有空闲空间可以分配的时候,会使用虚拟内存,即把不使用的swap到磁盘上,这时候过度的换页开销会是的性能大幅度下降。

环境变量

环境字符串的形式是name=value,ISO C定义的获取环境变量的函数getenv和设置环境变量的三个函数putenv、setenv、unsetenv,声明如下:

PS: 栈顶向上一般由environ指针和环境表(多个name=value指针)构成

#include <stdlib.h>
char *getenv(const char *name);
//putenv返回0表示成功,否则返回非0;参数中的str为name=value格式,如果name已存在则先删除原定义
int putenv(char *str);
//setenv和unsetenv返回0表示成功,出错返回-1,rewrite参数非0的话会覆盖,否则不覆盖且不报错
int setenv(const char *name, const char *value, int rewrite);
int unsetenv(const char *name);//删除name定义,name不存在也不报错

注意事项

  1. putenv可以使用格式为name=value格式的字符串变量作为函数的实际参数,此时系统直接将该变量存入环境变量,不再为其分配内存,所以该变量不能为局部变量等,应该为全局变量,防止退出函数后环境变量不可用;而setenv和putenv(字符常量)的时候,系统会自动分配内存
  2. 函数表在修改时的具体操作
    1. 删除的时候比较容易,找到对应的指针,然后将后续指针都往前挪动即可
    2. 增加和修改时,由于环境变量在进程空间的最顶端,不能进行扩张,故有一下情况
      1. 修改现有name的value内容比原来的短时,直接修改
      2. 修改现有name的value内容比原来长的时候,先新分配内容,再将原来的指针指向新的区域
      3. 如果是新增name,且是第一次新增的话,调用malloc函数为新的环境表分配空间,并将原来的环境表复制到新的空间内,然后将新增的放于表尾,同时在放一个空指针在后面,并将environ指针指向新的表,如果原来的环境表在栈之上,就会把表移动到堆中,但此时大多数指针仍指向栈顶之上的name=value字符串
      4. 如果不是第一次新增name的话,说明之前已经预留了空指针,此时只需要realloc一个指针的空间,并将上次的空指针修改指向新name=value的字符串,再将新申请的空间置空即可。

跨函数跳转setjmp和longjmp

预想一个场景,如shell。shell中需要解释命令,会有个while循环一直获取并解析命令,如果解释命令的函数有很多层,那么在某一层遇到错误的时候,只希望打印错误信息然后继续执行,这时候就需要逐层函数判断返回值,会很麻烦,因而引出了这两个函数。这两个函数常用于异常的处理方面。函数声明如下:

#include <setjmp.h>
int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);
/*
*longjmp会跳转到setjmp所在位置
*setjmp的返回值会是longjmp的val值
*一般而言jmp_buf需要是全局的,用于回复栈状态的所有信息
*/

注意

这两个函数比较危险,特别是在C++中,有许多不兼容的情况,谨慎使用

  1. 自动变量(近似局部变量)、寄存器变量和易失变量

    在回滚到setjmp的位置后,局部自动变量是否会析构,最初的自动变量、寄存器变量和易失变量是否也会回滚到对应的值,这是不能确定的,即undefined behavior。如果不想回滚可以声明变量为volatile或者声明为全局或静态变量。

补充知识:register关键字声明尽可能将该变量放在寄存器中,该变量需是小于等于整形大小的单个变量,且register变量不能取地址,因为不一定放在内存中。而在C++中可以取址,如果有这个操作,那么该变量一定会被放在内存中

进程的资源限制

进程的资源限制课以用getrlimit和setrlimit来查询和修改

#include <sys/resource.h>
int getrlimit(int resource, struct rlimit *rlptr);
int setrlimit(int resource, const struct rlimit *rlptr);
//若成功两个函数都返回0,否则返回非0
//结构体的定义如下
struct rlimit{
    rlimt_t rlim_cur;/* soft limit: current limit */
    rlimt_t rlim_max;/* hard limit: maximum value for rlim_cur */
};
//硬限制的降低不可逆,只有超级用户进程可以提高硬限制

总结

内容大致是进程的运行、终止,进程的存储空间分布和各个部分的介绍,最后是进程内部的非局部转义的方法以及资源限制方面的功能。