UNIX Process Environment

概述

这一部分介绍的是进程运行的环境。主要包括进程执行时,main函数是如何被调用的,命令行参数如何传递给进程的,进程的存储空间结构,如何分配存储空间,环境变量的使用,以及进程是怎么终止的。

main函数和argc, argv

C语言总是从main函数开始执行,C语言中main有两个原型:

1
2
int main(void);
int main(int argc, char *argv[]);

其中argc是命令行参数的个数,argv是一个指针数组,ISO C和POSIX.1都要求argv[argc]设置为NULL,所以可以用它作为参数处理的循环终止条件。关于更多指针的信息可以查看
当内核执行C程序(使用一个exec函数)时,在调用main函数之前设置一个特殊的启动例程。可执行程序文件将这个启动例程指定为程序的起始地址,这是由link editor设置的,它会被C编译器调用。启动例程从内核命令获得命令行参数和环境变量值,然后为调用main函数做好准备。

关于更多C和C++中main的介绍,可以查看C/C++ main argc argv

全局变量environ

每个C程序都会接收到一个environment list,和argv一样,它是一个指针数组。每个指针指向一个以null结束的C字符串的地址,这个指针数组的地址存放在全局变量environ中:

1
extern char **environ;

在历史上,UNIX大多支持三个参数的main函数,第三个参数就是environment list:

1
int main(int argc, char *argv[], char *envp[]);

但是ISO C规定main只能有两个参数,POSIX.1也就规定使用全局变量environ而不是第三个参数。如果要查看所有的环境变量时,使用environ,而访问某个特定的环境变量时,使用getenvsetenv

进程终止

总共有八种方式可以让进程终止,包括五种正常和三种异常,前五种是正常终止,后五种是异常终止:

  1. main返回
  2. 调用exit
  3. 调用_exit或者_Exit
  4. 最后一个线程从其启动例程返回
  5. 最后一个线程调用pthread_exit
  6. 调用abort
  7. 接到一个signal
  8. 最后一个线程对取消请求做出响应

exit函数

函数原型

1
2
3
4
5
6
7
8
#include <stdlib.h>

void exit(int status);
void _Exit(int status);

#include <unistd.h>

void _exit(int status);

性质

  1. exit_Exit是ISO C的内容,而_exit是POSIX.1的内容
  2. 它们都用于正常终止一个程序,_Exit_exit立刻进入内核,而exit先执行一些清理操作,然后返回内核。exit函数总是执行一个标准I/O库的关闭操作,对于所有打开的流调用fclose函数,所有带有未写缓冲的标准I/O流被flush。
  3. 三个退出函数都需要一个整形的参数,被称为exit status。
  4. 如果满足以下条件:
    • 调用这三个函数不带终止状态
    • main执行了一个不带返回值的return语句
    • main没有声明返回类型为整形,进程的终止状态是未定义的。
      那么这个进程的终止状态是未定义的。
  5. main返回返回一个整型值和用该值调用exit是等价的。对于某些C编译器和UNIX lint(1)程序来说,会产生警告信息,因为这些编译器并不了解main中的returnexit的作用是相同的。避开这种警告信息的一种方法是在main中使用return而不是exit,这样做的结果是UNIX grep命令无法找出程序中所有的exit调用。另一个方法是将main声明为void而不是int,然后调用exit,但是这不并不是标准,ISO C和POSIX.1定义main的返回值应当是带符号整形。

关于更多exit函数的内容,可以查看
关于exitreturn的内容,更多可以查看C/C++ exit and return

atexit

每个进程可以通过atexit register至多32个由exit自动调用的函数,这些函数被称为exit handler(终止处理程序)。

1
2
3
#include <stdlib.h>

int atexit(void (*function)(void));

  1. atexit的参数是一个函数地址,不会有返回值
  2. exit调用atexit register的程序的顺序和使用atexit进行register的顺序相反。
  3. ISO C和POSIX.1标准规定,exit首先调用各个exit handler,然后使用fclose关闭所有标准I/O流。
  4. POSIX.1对ISO C进行了扩展,如果程序调用了任何exec函数,清除exit handler。
  5. 内核执行一个程序的唯一方法是调用一个exec函数。进程自愿终止的唯一办法是显式或者隐式的(通过exit)调用_exit_Exit

C程序的存储空间布局

更多关于C程序存储空间布局可以查看C/C++ program memory layout

共享库

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

内存空间分配

ISO C说明了三个用于memory allocation的函数,malloc, callocrealloc,它们的原型如下,更多关于C中malloc的内容可以查看C/C++ malloc(alloc) free new and delete

malloc, callocrealloc原型

1
2
3
4
5
6
7
#include <stdlib.h>

void *malloc(size_t size);
void free(void *ptr);
void *calloc(size_t nmemb, size_t size);
void *realloc(void *ptr, size_t size);
void *reallocarray(void *ptr, size_t nmemb, size_t size);

malloc, callocrealloc属性

  1. malloc,分配指定字节的内存空间,初始值不定。
  2. calloc,为指定长度的固定数量的对象分配空间,每一个bit都被初始化为0。
  3. realloc,增加或者减少已经分配的内存空间的大小。当这个大小增加时,可能需要将之前分配的空间中的数据移到另一个足够大的区域以便于增加大小,新增加的区域内的值是不确定的。
  4. 这三个函数返回的指针一定是对齐的,保证它可以用于任何对象。比如double的要求最严格,需要从8的倍数的地址单元开始,这三个函数返回的地址一定满足这个要求。
  5. 它们的返回类型都是void*,需要使用强制类型转换。
  6. realloc函数可以增加或者减少之前分配的内存空间的大小。比如分配了一个固定大小的数组,后来发小它不够用了,可以使用realloc对它进行扩充,如果原有的存储后有足够的大小进行扩充,则可以在原存储区的位置上向高地址进行扩充,无需移动原有数组,返回和传入相同的指针。如果原来的内存空间后没有足够的空间,就重新分配一个足够大的内存空间,再将原有数据的内容复制过去,然后释放原来的内存空间,返回新的指针。
  7. realloc传入的参数是存储区的新长度。如果传入的ptr参数是NULL指针,那就退化成了malloc
  8. free可以释放ptr指向的内存空间,释放的空间通常送入可用内存池,之后可以通过这三个函数重新分配。
  9. mallocfree底层通常使用sbrk系统调用实现,这个系统调用扩充或者减小进程的堆,虽然sbrk可以扩充或者缩小进程的堆,但是一般mallocfree的实现不会减少进程的内存空间,释放的内存空间保存在malloc池中,而不是交给内核。
  10. 大多数实现分配的空间要比请求的空间大一些,因为需要存储一些管理信息,如block的大小,指向下一个block的指针等等。因此,如果对超过一个分配区域的内存进行读写的话,会造成很严重的错误。
  11. free一个已经释放了的块,free的不是alloc函数的返回值,没有进行free等等,都有可能造成很严重的后果。

环境变量

环境变量的形式是:
name = value
UNIX内核并不使用环境变量,通常都是应用程序使用这些环境变量。比如shell使用了大量的环境变量。

标准定义

ISO C定义了getenv函数可以获取环境变量。但是ISO C没有定义任何环境变量,SUS环境变量包括POSIX.1和XSI环境变量。
除了获取环境变量,有时候我们也需要设置环境变量。ISO C没有定义获取环境变量的函数。SUS除了定义了ISO C,还定义了putenv, setenvunsetenv对环境变量进行操作。

putenv,setenvunsetenv原型

1
2
3
4
5
#include <stdlib.h>

int putenv(char *string);
int setenv(const char *name, const char *value, int overwrite);
int unsetenv(const char *name);

putenv,setenvunsetenv性质

  1. putenv,创建或者重置一个name
  2. setenv,如果name不存在,创建name;如果name存在,rewrite不为零,重写,rewrite为零,不进行重写。
  3. unsetenv,移除name的定义。如果name不存在,不会出错。
  4. 环境变量的修改和增加可能会遇到一些问题。因为环境变量存放在进程地址空间的最上面的一个不可扩展的空间。
    如果修改一个存在的name
    当新的value的长度小于等于原来的value长度时,直接覆盖就行;
    当新的value的长度大于原来的value长度时,只需要给新的name-value字符串分配空间就行了。使用malloc为新的字符串分配空间,然后将该字符串复制到新的空间,让environment list中name的指针指向新分配的存放字符串的空间即可。

如果增加一个新的name,不仅需要给新的name-value分配空间,指针数组的元素也增加了,还需要给指针数组分配新的空间。首先给新的name-value字符串分配空间,调用malloc先为字符串分配空间,然后将该字符串复制到这个空间:
如果这是第一次增加一个name,必须调用malloc为指针数组增加空间。将原来的指针数组复制到新分配的空间中,然后将新字符串的指针放在指针数组的尾部,然后存放一个空指针。最后让全局变量char **environ指向这个指针数组。如果原来的指针数组存放在栈顶之上,需要将它复制到堆中。需要注意的是,这个指针数组中的未修改的环境变量的指针还是指向栈顶的字符串上。
如果这不是第一个添加name,只需要使用realloc重新多分配一个指针数组的空间即可,然后将它指向新字符串的地址即可。

setjmplongjmp

setjmplongjmp原型

1
2
3
4
#include <setjmp.h>

int setjmp(jmp_buf env);
void longjmp(jmp_buf env, int val);

setjmplongjmp属性

  1. 自动变量存储在每个函数的栈帧中。
  2. setjmplongjmp实在栈上跳过若干调用栈,返回到当前函数调用路径上的某个函数中。

getrlimitsetrlimit

每个进程能使用的资源都是有限的,可以使用getrlimtsetrlimit进行修改。它们都是XSI扩展,不是ISO C的定义。有些资源可以设置为RLIM_INFINITY,表示无限。

getrlimitsetrlimit性质

  1. 任何一个进程都可以将rlim_cur改为小于等于rlim_max
  2. 任何一个进程都可以将rlim_max改小,但是不能小于rlim_cur,且这个更改是不可逆的。
  3. 只有root用户可以更改rlim_max

它们的原型原型如下:

getrlimitsetrlimit原型

1
2
3
4
5
#include <sys/time.h>
#include <sys/resource.h>

int getrlimit(int resource, struct rlimit *rlim);
int setrlimit(int resource, const struct rlimit *rlim);

resource种类

  • RLIMIT_AS
  • RLIMIT_VMEM
  • RLIMIT_DATA
  • RLIMIT_SWAP
  • RLIMIT_STACK
  • RLIMIT_NPROC
  • RLIMIT_FSIZE
  • RLIMIT_NOFILE
  • RLIMIT_NICE

结构体

结构体struct rlimt的定义如下:

1
2
3
4
5
struct rlimit
{
rlim_t rlim_cur; // soft limit
rlim_t rlim_max; // hard limit
}

参考文献

  1. 《APUE》第三版