进程环境

进程环境

1. main函数

C程序总是从main函数开始执行,main函数的原型为:

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

其中argc是命令行参数的数目,argv是指向命令行参数的字符串指针数组。
当内核执行C程序时(使用一个exec函数),在调用mian前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址——这是由连接编辑器,而连接编辑器则由C编译器调用。启动例程从内核取得命令行参数环境变量值,然后为按上述方式调用main函数做好安排。

2. 进程终止

正常终止方式:
(1) 从main函数返回;
(2) 调用exit
(3) 调用_exit_Exit
(4) 最后一个线程从启动例程返回。
(5) 最后一个线程调用pthread_exit
异常终止方式:
(6) 调用abort
(7) 接到一个信号。
(8) 最后一个线程对取消请求作出响应。

上节提到的启动例程时这样编写的,当main函数返回后,立即调用exit函数。如果启动例程以C代码表示(实际上该例程常常以汇编语言编写),则它调用main函数的形式可能是:

exit(main(argc, argv));
2.1 退出函数

3个函数用于正常终止一个程序:_exit和_Exit立即进入内核,exit则先执行一些清理处理,然后返回内核。

#include <stdlib.h>
void exit(int status);
void _Exit(int status);
#include <unistd.h>
void _exit(int status);

使用不同的头文件是因为exit_Exit是由ISO C说明的,_exit是由POSIX.1说明的。

由于历史原因,exit函数总是执行一个标准I/O库的清理关闭操作:对于所有打开的流调用fclose函数,这造成输出缓冲中所有数据都被冲洗(写到文件上)。
3个退出函数都带一个整形参数,称为终止状态(或退出状态,exit status)。大多数UNIX系统都提供检查进程终止状态的方法。如果(a)调用这些函数时不太终止状态,(b) main函数提供了一个无返回值的return语句,(3)main函数没有声明返回类型为整形,则该进程终止状态是未定义的。但是,若main函数的返回值类型为整形,并且main执行到最后一条语句时返回(隐式返回),那么该进程的终止状态是0。

main函数返回一个整形值与用该值直接调用exit是等价的。在main函数中exit(0),等价于return 0;

return 0 时启动例程调用exit(main(argc, argv));

2.2 函数atexit

终止处理函数(exit handler)是在exit函数中自动调用的函数,它们被用来在程序终止时,做一些清理工作或其他工作。使用atexit来注册这些终止处理函数,一个进程最多可以注册32个函数。

#include <stdlib.h>
int atexit(void (* func)(void));
// 返回值:若成功,返回0;若失败,返回非0。

其中,atexit函数的参数是一个函数地址,这个函数无参数无返回值。exit调用这些终止处理函数的顺序与这些函数的注册顺序相反(栈),一个函数多次注册,也会多次调用。
根据ISO C和POSIX.1,exit首先调用各终止处理函数,然后关闭所有打开的标准I/O流(通过fclose)。POSIX.1扩展了ISO C标准,它说明,如果程序调用exec函数族的任一函数,则将清除所有已安装的终止处理程序。
注意:内核使程序执行的唯一办法是调用一个exec函数。进程资源终止的唯一方法是显式或隐式的(通过exit)调用_exit_Exit函数。进程也可以非自愿的由一个信号使其终止。

3. 命令行参数

当执行一个程序时,调用exec可以将命令行参数传递给新程序。
ISO C 和 POSIX.1 都要求argv[argc]是一个空指针。

4. 环境表

每个程序都接收到一张环境表。与参数表一样,环境表也是一个字符指针数组,其中每个指针都包含一个以null结束的C字符串地址。全局变量environ则包含了该数组指针的地址:

extern char **environ;
environ[1] = "name=value";

通常用getenvputenv函数来访问特定的环境变量,而不是用environ变量。但是,如果要查看整个环境,则必须使用environ指针。

5. C程序的存储空间布局

历史沿袭至今,C程序一直由下列几部分组成:

  • 正文段:CPU执行的机器指令部分。在存储器中只存有一个副本,供各执行进程读取,一般为只读属性,防止程序对齐修改。
  • 初始化数据段:分为初始化数据和非初始化数据部分(bss段)。初始化数据(全局变量的初始化)由exec程序读文件初始化,非初始化数据(全局变量的默认初始化)初始化为0或空指针。
  • 栈:自动变量和每次函数调用时所保存的信息(函数调用栈)都放在此栈中,每次函数调用时。其返回地址以及调用者的环境信息(如某些及其寄存器中的值)都放在栈中。然后,最近调用的函数在栈上为为其自动和临时变量分配存储空间。通过以这种方式使用栈,C递归函数可以工作。递归函数每次调用自己时,就先形成一个新的帧栈,因此一次函数实例调用中的变量不会影响另一次实例调用的变量。
  • 堆:通常在堆中进行动态存储分配。

6. 共享库

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

7. 存储空间分配

ISO C说明3个用于存储空间动态分配的函数。

在堆上进行动态分配

(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);
// 若成功,返回非空指针;若出错,返回NULL
void free(void *ptr);

这3个分配函数所返回的指针一定是适当对齐的,使其可用于任何数据对象。例如,在一个特定的系统上,如果最苛刻的对齐要求是,double必须在8的倍数地址单元处开始,那么这3个函数返回的指针都应该这样对齐。
函数free释放ptr指向的存储空间。被释放的存储空间通常被送入可用存储区池,不返回给内核,以后,可在调用上述三个分配函数是再分配。
realloc函数使我们可以增减以前分配的存储区长度(最常见的方法是增加该区)。例如,如果先为一个数组分配存储空间,该数组长度为512,然后在运行时填充它(再分配),如果当前指针所指向存储区未分配存储空间不足,则分配一个新的足够大的存储空间,并将原先的存储区的数据拷贝到新的存储区中,然后释放原存储区,返回新的存储区的指针。因为存储区可能会移动位置,在调用realloc后,不应该使用原先的指针访问,若空间足够,则向高地址方向扩充存储区,并返回与传入指针相同的指针。

8. 函数setjmplongjmp

在C中,goto语句是不能跨越函数的,而执行这种功能类型的跳转函数是setjmplongjmp
这两个函数对于处理发生在很深层嵌套函数调用出错情况是非常有用的。
当在多层嵌套调用发生错误时,通过逐层检查返回值来控制程序执行往往很麻烦。使用非局部的"goto"–在某一函数内跳转,setjmplongjmp在帧栈上跳过若干调用帧,返回到当前函数调用路径的某一函数中。

int setjmp(jmp_buf env);
// 返回值:若直接调用,返回0;若从longjup返回,返回非0.
void longjmp(jup_buf env, int val);

在希望返回到的位置调用setjmp。直接调用返回0。setjmp函数的参数env的类型是一个特殊类型jmp_buf。这一数据类型是某种形式的数组,其中存放在调用longjmp时能用来恢复帧栈的所有信息。因为在另一个函数中也需要引用env,所以通常将env定义为全局变量。
当检查到一个错误时,在希望执行返回的位置使用2个参数调用longjmp,第一个参数时env,保留恢复返回到的函数的帧栈信息,第二个参数val是一个非0值,即为setjmp在返回时的返回值。

  1. 自动变量、寄存器变量和易失变量
    当调用longjmp返回到main函数时,自动变量和寄存器变量的值在所有标准中都是不确定的,即无法确定main函数中的自动变量和寄存器变量的值是否回滚到之前(调用setjmp返回0时)的值,还是保持当前写入的值,在大多数的实现中,都不会回滚自动变量和寄存器变量。如果你有一个自动变量,而又不想使其值回滚,则可以定义其具有volatile属性。声明为全局变量或者静态变量的值在执行longjmp时保持不变。

9 小结

理解UNIX系统环境中C程序的环境时理解UNIX系统进程控制特性的先决条件。本章说明了一个进程是如何启动和终止的,如何向其传递参数表和环境。虽然参数表和环境都不是由内核进行解释的,但内核起到了从exec的调用者将这两者传递给新进程的作用。
本章也说明了C程序典型的存储空间布局,以及一个进程如何动态的分配和释放存储空间。本章也介绍了setjmplongjmp函数,它们提供了一种在进程内非局部转移的方法。

习题

  1. printf函数的结果何时才被真正输出?
    printf函数格式化输出到标准输出,标准一般为行缓存的标准IO流,当向标准输出写入null字符时,缓冲中的数据被真正输出,或值调用fflush冲洗缓冲区,这可能出现在进程执行时,也可能在调用exit函数时进行标准I/O流的清理,冲洗缓冲区内容。
  2. 是否有方法不使用(a)参数传递,(b)全局变量这两种方法将main函数中的参数argc和argv传递给它所调用的其他函数?
  3. 在有些UNIX系统的实现中执行程序时访问不到其数据段的0单元;这是一种有意的安排,为什么?
    数据段存放程序中需要明确初始化的全局/静态变量。当解引用一个空指针时,试图访问数据段的0单元,在执行程序时访问不到其数据段的0单元情况下,会出现访问出错,从而终止进程执行。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值