进程环境
在本篇文章中将要了解进程的环境。主要包括当程序执行时main函数如何被调用;命令行参数如何传递给新程序;典型的存储空间布局样式;进程如何使用环境变量;进程的各种不同终止方式。
C程序的启动和终止
一个c程序的启动与终止过程如下:
内核使程序执行的唯一方法是调用一个exec函数。进程自愿终止的唯一方法是显示或隐式地(通过调用exit)调用_exit或_Exit,进程也可以非自愿的由一个信号使其终止。
1、main函数
对于一个c程序总是从main函数开始执行的,main函数的原型如下:
int main(int argc, char *argv[]);
当内核执行C程序时,在调用main前先调用一个特殊的启动例程。可执行程序文件将此启动例程指定为程序的起始地址——这是由连接编辑器设置的,而连接编辑器由C编译器调用。启动例程从内核取得命令行参数和环境变量值,然后为按上述方式调用main函数做好安排。
2、进程终止
有8种方式使进程终止,其中有5种为正常终止:
• 从 main 返回
• 调用 exit
• 调用_exit 或者_Exit
• 最后一个线程从启动例程返回
• 从最后一个线程调用 pthread_exit
3种异常终止方式为:
• 调用abort
• 接到一个信号
• 最后一个线程对取消请求做出响应
其中有三个函数用于正常终止一个进程,根据上图可以看到,其中_exit 和 _Exit 立即进入内核,exit 自动调用终止处理程序(通过函数atexit来登记这些函数)来执行一些处理,然后返回内核。如下:
#include <stdlib.h>
void exit(int status);
void _Exit(int status);
int atexit(void (*func)(void));
#include <unistd.h>
void _exit(int status);
例如:
#include "apue.h"
static void my_exit1(void);
static void my_exit2(void);
int
main(void)
{
if (atexit(my_exit2) != 0)
err_sys("can't register my_exit2");
if (atexit(my_exit1) != 0)
err_sys("can't register my_exit1");
if (atexit(my_exit1) != 0)
err_sys("can't register my_exit1");
printf("main is done\n");
return(0);
}
static void
my_exit1(void)
{
printf("first exit handler\n");
}
static void
my_exit2(void)
{
printf("second exit handler\n");
}
运行如下:
xucheng@xucheng-Inspiron-5520:~/xc$ ./a.out
main is done
first exit handler
first exit handler
second exit handler
可以看出
终止处理程序每调用一次就会执行一次,并且调用的顺序与执行的顺序相反(栈)。
C程序的存储空间布局
典型的存储空间布局如下:
包含以下几个方面:
• 正文段:这是由CPU执行的机器指令部分。通常,正文段是只读共享的,在存储器中只需一个副本。
• 初始化数据段:通常将此段称为数据段,它包含了程序中需要明确赋初值的变量。
例如,任何函数之外的声明 int max=99;
• 未初始化数据段:通常将此段称为 bss 段,意思是”由符号开始的块“,在程序开始执行前,内核将此段中数据初始化为0或空指针。
• 栈:栈空间向低地址增长。自动变量以及每次函数调用时所需保存的信息都存放在此段中。每次函数调用时,其返回地址、以及调用者的环境信息(例如某些机器寄存器)都存放在栈中。然后,新被调用的函数在栈上为其自动和临时变量分配存储空间。通过以这种方式使用栈, C函数可以递归调用。
• 堆:通常在堆中进行动态存储分配。由于历史上形成的惯例,堆位于非初始化数据段顶和栈底之间。
环境变量
每个程序都收到一张环境表。与参数表一样,环境表也是一个字符指针数组,每个指针包含一个以null结尾的c字符串的地址。全局变量environ包含了该指针数组的地址。
extern char**environ;
//environ 称为环境指针,指针数组为环境表,其中各指针指向的字符串为环境字符串
例如:
如上所示环境变量字符串的形式是name=value,通常我们要访问环境变量的值是通过函数,而不是访问environ指针,C中定义了以下几个函数来查看环境变量的值以及修改环境变量,如下所示:
//查看环境表的函数
#include <stdlib.h>
char* getenv(const char* name);
//修改环境表的函数:
#include <stdlib.h>
int putenv(const char* str);//成功返回 0,出错返回非0
int setenv(const char* name,const char* value,int rewrite);//成功返回 0,出错返回-1
void unsetenv(const char* name);//成功返回 0,出错返回-1
/*
• putenv取形式为name=value的字符串,将其放到环境表中。如果name已经存在,则先删除其原来的定义
• setenv将name设置为value。如果在环境中name已经存在,那么(a)若rewrite非0,则首先删除其现存的定义;(b)若rewrite为0,则不删除其现存定义(name不设置为新的value,而且也不出错)
• unsetenv删除name的定义。即使不存在这种定义也不算出错
*/
对于以上函数的运用说明,例如:
#include <stdio.h>
#include <stdlib.h>
int
main(void)
{
char *envVal;
char *myenv;
if ((envVal = getenv("PATH")) == NULL) {
printf("not environment variable PATH\n");
} else {
printf("PATH=%s\n", envVal);
}
if (setenv("myenv", "xucheng", 0) == -1) {
printf("setenv error\n");
} else {
myenv = getenv("myenv");
printf("myenv=%s\n", myenv);
}
if (putenv("myenv1=hello") != 0) {
printf("putenv error\n");
} else {
myenv = getenv("myenv1");
printf("myenv1=%s\n", myenv);
}
exit(0);
}
编译后结果如下:
xucheng@xucheng-Inspiron-5520:~/xc gccenvironment.cxucheng@xucheng−Inspiron−5520: /xc g c c e n v i r o n m e n t . c x u c h e n g @ x u c h e n g − I n s p i r o n − 5520 : / x c ./a.out
PATH=/home/xucheng/bin:/home/xucheng/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin
myenv=xucheng
myenv1=hello
NOTE:p**utenv和setenv之间的区别。setenv必须分配存储空间,以便依据其参数创建name=value字符串。putenv可以自由的将传递给它的参数字符串直接放到环境中。**因此将存放在栈中的字符串作为参数传递给putenv会发生错误,原因是从当前函数返回时,其栈帧占用的存储区可能被重用。
在存储空间布局中可以知道,环境变量与环境表通常放在进程存储空间的最顶部,删除一个字符串很简单,只要在环境表中找到对应的指针,然后将其后面的指针依次向首部移动一个位置。但是增加一个字符串或者改变一个字符串就不是那么容易,由于环境字符串和环境表通常在进程空间的最顶端,所以他不能向上扩展;同时他也不能移动在他之下的各个栈帧,所以它也不能向下扩展。所以通常的做法如下:
如果修改一个现有的name:
1、如果新value的长度小于或等于现有value的长度,只要将新字符串复制到原字符串的空间中就可以。
2、如果新value的长度大于现有value的长度,则必须调用malloc为新字符串分配空间,然后将新字符串复制到新空间中,并且让环境表中指向name的指针指向新分配的空间。
如果新增加一个name:
对于新增加一个name,首先调用malloc为新字符串分配空间,然后将新字符串name=value复制到新空间中。
1、如果这是第一次增加name,首先必须调用malloc为环境表分配空间,然后将原来的环境表复制到新分配的空间,然后在表尾加上指向新字符串name=value的指针,并且在最后加上空指针,最后让指针指向environ指向新指针表。此时可以看到,虽然环境表从原来的栈顶移到了堆中,但是其中的大多数指针仍然指向栈顶。
2、如果这不是第一次增加name,那么说明在之前已经调用malloc在堆中分配了空间,所以此时只要调用realloc,分配比原来空间多一个指针的空间就可以,然后将指向新字符串name=value的指针添加到表尾。