进程环境
7.1 引言
为下一章做准备,先了解进程环境。在本章将会解决一下问题:
当程序执行时,其main函数是如何被调用的;
命令行参数是如何传递给新程序的;
典型的存储空间布局是什么样式;
如何分配另外的存储空间;
进程如何使用环境变量;
进程的各种不同终止方式;
说明longjmp和setjmp函数以及他们与栈的交互作用;
查看进程的资源限制。
7.2 main函数
C程序总是从main函数开始执行。main函数原型是:
int main(int argc ,char *argv[]);
当内核执行C程序时,在调用main函数前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址(可以看出一个可执行文件的起始执行函数是启动例程,其次才是main函数)。启动例程是链接器连接到mian函数前面的,C编译器调用连接器。
启动例程从内核取得命令行参数和环境变量,为上述方式调用main函数做好安排。
7.3 进程终止
有8种方式使进程终止(termination),其中5种为正常终止:
- 1.从main函数返回;
- 2.调用exit;
- 3.调用_exit或_Exit;
- 4.最后一个线程从其启动例程返回;
- 5.最后一个线程调用pthread_exit。
异常终止有3种方式:
- 6.调用abort;
- 7.接收到一个信号;
- 8.最后一个线程对取消请求作出响应。
启动例程是这样编写的,它使得从main返回后立即调用exit函数。它通常使用汇编编写,如果写成C的形式应该是:exit(main(argc ,argv);
7.3.1 退出函数
3个函数用于正常终止一个程序:
#include <stdlib.h>
void exit(int status); //先清理再进入内核(对所有打开的流进行fclose操作,这就造成数据冲洗)
void _EXIT(int status); //立即进入内核
#include <unistd.h>
void _exit(int status); //立即进入内核
3个退出函数都带一个整形参数,称为终止状态(或退出状态:exit status)。大多数shell都提供检查进程终止状态的方法。
mian函数返回一个整型值与用该值调用exit是等价的。于是exit(0)等价于return(0);
7.3.2 函数atexit
一个进程最多可以登记32个函数,这些函数由exit自动调用。我们称这些函数为终止处理程序(exit handler),并调用atexit函数来登记这些函数。
#include <stdlib.h>
int atexit(void (*func)(void));
其中,atexit的参数是一个函数地址,这个函数不需要向它传递参数也没有返回值。exit调用这些函数的顺序与它们登记的时候的顺序相反。同一个函数如果被登记多次,它也会被调用多次。
下图指示了一个程序的启动和终止:
从上图中可以看出:
- 程序执行的唯一方法是调用一个exec函数。
- 进程自愿终止的方法是显式或隐式地(通过调用exit)调用_exit或_EXIT。
- exit先调用exit handler再通过fclose关闭打开的流。
- 进程也可以非自愿地由一个信号使其终止。
7.4 命令行参数
当执行一个程序时,调用exec的进程可以将命令行参数传递给该新程序,shell就是这样的。
ISO C和POSIX.1都要求argv[argc]是一个空指针。因此可以用下面函数输出参数:
for(i=0;argv[i]!=NULL;i++)
{
printf(argv[i]);
}
程序echo就有这样的功能,但是他不回显第0个参数。
7.5 环境表
每个进程都接收到一张环境表。与参数表一样,环境表也是一个字符指针数组,其中每个指针包含一个以null结束的字符串地址。全局变量environ包含了该指针数组的地址:
extren char **environ;
环境表的格式为:name=value的形式组成。
通常用getenv和putenv函数来访问特定的环境变量,而不是直接使用environ,但是如果要查看整个环境,必须使用environ指针。
7.6 C程序的存储空间布局
C程序的组成:
段 | 内容 |
---|---|
正文段 | 这是由CPU执行的机器指令部分。通常,正文是共享的,所以即使是频繁执行的程序在内存中也只需要一个副本。它是只读的,防止程序由于意外而被修改 |
初始化数据(数据段) | 它包含了程序中需要明确地赋初值的变量。如int maxcount=99; |
未初始化数据(bss) | bss的名称来自于早期汇编程序的一个操作符合,意思是“由符号开始的块”(block started by symbol),在程序开始执行前,由内核将此段中的数据初始化为0或空指针。如int sum[100]; |
栈 | 自动变量以及每次函数调用时需要保存的信息都放在此段。 |
堆 | 通常在堆中进行动态分配,堆位于未初始化数据段和栈之间。 |
一种用于简便说明的存储空间布局如上如图:
可以看出,存入磁盘的段只有正文段和初始化数据段,bss段是内核在开始运行前将它们设置为0。
size分别返回这3个段的长度,dec和hex制定出了这3个段的十进制和十六进制的总长。
7.7 共享库
共享库使得可执行文件中不再需要包含公用的库函数,而只需要在所有进程都可以引用的存储区中保存这种库例程的一个副本。
程序第一次执行或者第一次调用某个库函数时,用动态链接的方法将程序与共享库函数相链接。这减少了每个可执行文件的长度,但增加了一些运行时间开销,这种时间开销发生在该程序第一次被执行时,或者每个共享库函数第一次被调用时。
共享库的另一个优点是可以用库函数更新程序。
可以使用gcc的-static选项选择静态链接。
7.8 存储空间分配
ISO C说明了3个用于存储空间动态分配的函数:
#include <stdlib.h>
void *malloc(size_t size);//分配空间,初始值不确定。
void *calloc(size_t nobj,size_t size);//为指定长度的对象分配存储空间,初始值为0。
void *realloc(void *ptr,size_t newsize);//重新分配ptr为newsize大小。
void free(void *ptr);
使用free函数释放alloc函数分配的空间。被释放的空间通常被送入可用存储区池,可以以后再用。
注意:
- 大多数实现所分配的存储空间比所要求的要稍大一些,额外的空间用来记录管理信息–分配块的长度、指向下一个分配块的指针等。因此:如果在所分配的空间之前或之后进行写操作,则可能破坏其他空间或本空间的数据!
- 释放了一个已经释放过了的块会产生致命错误。
另外,还有一些可替代的存储空间动态分配函数。
7.9 环境变量
环境字符串的形式是:name=value
UNIX内核并不查看这些字符串,他们的解释完全取决于各个应用程序。
getenv函数用于取得环境变量:
#include <stdlib.h>
char *getenv(const char *name);//返回指定name的value字符串指针
以下函数用于设置环境变量:
#include <stdlib.h>
int putenv(char *str);
int setenv(const char *name,const char *value,int rewrite);
int unsetenv(const char *name);
- putenv 使用name=value形式的字符串,将其放到环境表中,如果已经存在则更新。
- setenv 将name设置为value;如果name已经存在,那么若rewrite非0,则首先删除现有的定义;若rewrite为0,则不删除现有定义。
- unsetenv删除name定义;
环境表(指向name=value字符串的指针数组)和环境字符串通常存放在进程存储空间的顶部(栈之上),并且它们通常都是占用进程地址空间的顶部,所以不能再向高地址空间方向(向上)扩展,也不能向低地址空间方向(向下)扩展。两者组合使得该空间的长度不能再增加。所以如果修改或增加name字段,操作比较复杂。
7.10 函数setjmp和longjmp
在c语言中,goto语句是不能跨越函数的,若要跨越函数跳转需要使用setjmp和longjmp函数,这对处理发生在很深的嵌套调用中的出错返回是很有用的。
考虑一种情况:
此时栈的情况如下:
现在更改如下:
#include <setjmp.h>
jmp_buf jmpBuffer;
//...
int main()
{
int ret_jmp;
//...
if((ret_jmp=setjmp(jmpBuffer))!=0) //这里的setjmp会返回多次。
printf("jmp return ,return value=%d.\n",ret_jmp);
//...
}
int do_line()
{
//...
cmd_add();
//...
}
void cmd_add()
{
//...
longjmp(jmpBuffer,ret_val);//这里将使得setjmp返回,第二个参数是返回值。
//...
}
此时的栈情况:
此时抛弃了cmd_add和do_line的栈帧,调用longjmp将导致setjmp返回,返回longjmp的第二个参数。
7.11 函数getrlimit和setrlimit
每个进程都有一组资源限制。其中一些可以用getrlimit和setrlimit函数查询和更改。
#include <sys/resource.h>
int getrlimit(int resource,struct rlimit *rlptr);
int setrlimit(int resource,const struct rlimit *rlptr);
进程的资源限制通常是在系统初始化时由0进程建立的,然后由后继进程继承。对这两个函数的每一次调用都指定一个资源以及一个指向下列结构的指针。
struct rlimit{
rlim_t rlim_cur; //软限制,当前的限制值
rlim_t rlim_max; //硬限制,限制了rlim_cur的最大值
};
修改资源限制时,须遵循下列3条规则:
- 任何一个进程都可以将一个软限制值更改为小于或等于其硬限制的值。
- 任何一个进程都可以降低其硬限制值,但必须大于或等于其软限制值。这种降低对普通用户而言是不可逆的。
- 只有超级用户进程可以提高硬限制值。
resource取值为下列表(不考虑具体unix实现):
限制 | 值 |
---|---|
RLIMIT_AS | 进程总的可用存储空间的最大长度。这影响sbrk函数和mmap函数。 |
RLIMIT_CORE | core文件最大字节数,若其值为0则组织创建core文件。 |
RLIMIT_CPU | CPU时间的最大量值(秒),当超过此软限制时,向该进程发生SIGXCPU信号。 |
RLIMIT_DATA | 数据段的最大字节长度。 |
RLIMIT_FSIZE | 可创建的文件最大字节长度。当超过此软限制时,则向该进程发送SIGXFSZ信号。 |
RLIMIT_MEMLOCK | 一个进程使用mlock能够锁定在存储空间中的最大字节长度。 |
RLIMIT_MSGQUEUE | 进程为POSIX消息队列可分配的最大存储字节数。 |
RLIMIT_NICE | 为了影响进程的调度优先级,nice值可设置的最大限制。 |
RLIMIT_NOFILE | 每个进程能打开的最多文件数。 |
RLIMIT_NPROC | 实际用户ID可拥有的最大子进程数。 |
RLIMIT_NPTS | 用户可同时打开的伪终端的最大数目。 |
RLIMIT_RSS | 最大驻内存集字节长度 |
RLIMIT_SBSIZE | 一个用户可占用的套接字缓冲区的最大长度字节。 |
RLIMIT_SIGPENDING | 一个进程可排队的信号量最大数量。 |
RLIMIT_STACK | 栈的最大字节长度。 |
RLIMIT_SWAP | 用户可消耗的交换空间最大字节数。 |
RLIMIT_VMEM | 和RLIMIT_AS一样。 |
7.12 小结
理解UNIX系统中C程序的环境是理解UNIX系统进程控制特性的先决条件。
- 本章说明了一个进程是如何启动和终止的。
- 如何向进程传递参数表和环境。
- 虽然参数表和环境都不是由内核解释的,单内核起到了从exec的调用者将这两者传递给新进程的作用。
- 说明了C程序的典型存储空间布局,已经一个进程如何动态地分配和释放存储空间。
- 介绍了setjmp和longjmp函数,它们提供了进程内非局部转移的方法。
- 介绍了各种UNIX系统提供的资源限制功能。