C 语言程序总是从 main 函数开始执行,main()函数的原型是:
int main(void)
或者
int main(int argc, char *argv[])
如果需要向应用程序传参,则选择第二种写法。不知大家是否想过“谁”调用了 main()函数?事实上, 操作系统下的应用程序在运行main()函数之前需要先执行一段引导代码,最终由这段引导代码去调用应用程序的main()函数,我们在编写应用程序的时候,不用考虑引导代码的问题,在编译链接时,由链接器将引导代码链接到我们的应用程序当中,一起构成最终的可执行文件。
然而,程序是如何结束的呢?
程序结束其实就是进程终止,进程终止的方式通常有多种,大体上分为正常终止和异常终止,正常终止包括:
1、 main()函数中通过 return 语句返回来终止进程;
2、应用程序中调用 exit()函数终止进程;
3、应用程序中调用_exit()或_Exit()终止进程;
异常终止包括:
1、应用程序中调用 abort()函数终止进程;
2、进程接收到一个信号,譬如 SIGKILL 信号。
注册进程终止处理函数 atexit()
注册进程终止处理函数 atexit()
#include <stdlib.h>
int atexit(void (*function)(void));
函数参数和返回值含义如下:
function:函数指针,指向注册的函数,此函数无需传入参数、无返回值。
返回值:成功返回 0;失败返回非 0。
测试
#include <stdio.h>
#include <stdlib.h>
static void bye(void){
puts("Goodbye!");
}
int main(int argc, char *argv[]){
if (atexit(bye)) {
fprintf(stderr, "cannot set exit function\n");
exit(-1);
}
exit(0);
}
运行结果:
需要说明的是,如果程序当中使用了_exit()或_Exit()终止进程而并非是 exit()函数,那么将不会执行注册的终止处理函数。
概念
进程其实就是一个可执行程序的实例,这句话如何理解呢?可执行程序就是一个可执行文件,文件是一个静态的概念,存放磁盘中,如果可执行文件没有被运行,那它将不会产生什么作用,当它被运行之后,它将会对系统环境产生影响,所以可执行程序的实例就是可执行文件被运行。
进程是一个动态过程,而非静态文件,它是程序的一次运行过程,当应用程序被加载到内存中运行之后成为一个进程,当程序运行结束后也就意味着进程终止,即进程的一个生命周期。
进程号
Linux 系统下的每一个进程都有一个进程号(processID,简称 PID),进程号是一个正数,用于唯一标识系统中的某一个进程。在 Ubuntu 系统下执行 ps 命令可以查到系统中进程相关的一些信息,包括每个进程的进程号,如下所示:
上图中红框标识显示的便是每个进程所对应的进程号,进程号的作用就是用于唯一标识系统中某一个进程,在某些系统调用中,进程号可以作为传入参数、有时也可作为返回值。譬如系统调用 kill()允许调用者向某一个进程发送一个信号,如何表示这个进程呢?则是通过进程号进行标识。
在应用程序中,可通过系统调用 getpid()来获取本进程的进程号,其函数原型如下所示:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
函数返回值为 pid_t 类型变量,便是对应的进程号。
使用示例
使用 getpid()函数获取进程的进程号。
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main(void)
{
pid_t pid = getpid();
printf("本进程的 PID 为: %d\n", pid);
exit(0);
}
运行结果
除了 getpid()用于获取本进程的进程号之外,还可以使用 getppid()系统调用获取父进程的进程号,其函数原型如下所示:
#include <sys/types.h>
#include <unistd.h>
pid_t getppid(void);
返回值对应的便是父进程的进程号。
进程的环境变量
每一个进程都有一组与其相关的环境变量,这些环境变量以字符串形式存储在一个字符串数组列表中,把这个数组称为环境列表。其中每个字符串都是以“名称=值(name=value)”形式定义,所以环境变量是“名称-值”的成对集合,譬如在 shell 终端下可以使用 env 命令查看到 shell 进程的所有环境变量,如下所示:
使用 export 命令还可以添加一个新的环境变量或删除一个环境变量:
export LINUX_APP=123456 # 添加 LINUX_APP 环境变量
使用"export -n LINUX_APP"命令则可以删除 LINUX_APP 环境变量。
export -n LINUX_APP # 删除 LINUX_APP 环境变量
环境变量的作用
环境变量常见的用途之一是在 shell 中,每一个环境变量都有它所表示的含义,譬如 HOME 环境变量表示用户的家目录,USER 环境变量表示当前用户名,SHELL 环境变量表示 shell 解析器名称,PWD 环境变量表示当前所在目录等,在我们自己的应用程序当中,也可以使用环境变量。
应用程序中获取环境变量
在我们的应用程序当中也可以获取当前进程的环境变量,事实上,进程的环境变量是从其父进程中继承过来的,譬如在 shell 终端下执行一个应用程序,那么该进程的环境变量就是从其父进程(shell 进程)中继承过来的。新的进程在创建之前,会继承其父进程的环境变量副本。
环境变量存放在一个字符串数组中,在应用程序中,通过 environ 变量指向它,environ 是一个全局变量,在我们的应用程序中只需申明它即可使用,如下所示:
extern char **environ; // 申明外部全局变量 environ
测试
编写应用程序,获取进程的所有环境变量。
#include <stdio.h>
#include <stdlib.h>
extern char **environ;
int main(int argc, char *argv[]){
int i;
/* 打印进程的环境变量 */
for (i = 0; NULL != environ[i]; i++)
puts(environ[i]);
exit(0);
}
运行结果:
获取指定环境变量 getenv()
如果只想要获取某个指定的环境变量,可以使用库函数 getenv(),其函数原型如下所示:
#include <stdlib.h>
char *getenv(const char *name);
函数参数和返回值含义如下:
name:指定获取的环境变量名称。
返回值:如果存放该环境变量,则返回该环境变量的值对应字符串的指针;如果不存在该环境变量,则返回 NULL。
使用 getenv()需要注意,不应该去修改其返回的字符串,修改该字符串意味着修改了环境变量对应的值, Linux 提供了相应的修改函数,如果需要修改环境变量的值应该使用这些函数,不应直接改动该字符串。
使用示例
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[]){
const char *str_val = NULL;
if (2 > argc) {
fprintf(stderr, "Error: 请传入环境变量名称\n");
exit(-1);
}
/* 获取环境变量 */
str_val = getenv(argv[1]);
if (NULL == str_val) {
fprintf(stderr, "Error: 不存在[%s]环境变量\n", argv[1]);
exit(-1);
}
/* 打印环境变量的值 */
printf("环境变量的值: %s\n", str_val);
exit(0);
}
运行结果:
添加/删除/修改环境变量
C 语言函数库中提供了用于修改、添加、删除环境变量的函数,譬如 putenv()、setenv()、unsetenv()、 clearenv()函数等。
putenv()函数
putenv()函数可向进程的环境变量数组中添加一个新的环境变量,或者修改一个已经存在的环境变量对应的值,其函数原型如下所示:
#include <stdlib.h>
int putenv(char *string);
函数参数和返回值含义如下:
string:参数 string 是一个字符串指针,指向 name=value 形式的字符串。
返回值:成功返回 0;失败将返回非 0 值,并设置 errno。
该函数调用成功之后,参数 string 所指向的字符串就成为了进程环境变量的一部分了,换言之,putenv() 函数将设定 environ 变量(字符串数组)中的某个元素(字符串指针)指向该 string 字符串,而不是指向其副本,这里需要注意!因此,不能随意修改参数 string 所指向的内容,这将影响进程的环境变量,出于这种原因,参数 string 不应为自动变量(即在栈中分配的字符数组。
测试
使用 putenv()函数为当前进程添加一个环境变量。
#include <stdio.h>
#include <stdlib.h>
int main(int argc, char *argv[])
{
if (2 > argc) {
fprintf(stderr, "Error: 传入 name=value\n");
exit(-1);
}
/* 添加/修改环境变量 */
if (putenv(argv[1])) {
perror("putenv error");
exit(-1);
}
exit(0);
}
setenv()函数
setenv()函数可以替代 putenv()函数,用于向进程的环境变量列表中添加一个新的环境变量或修改现有环境变量对应的值,其函数原型如下所示:
#include <stdlib.h>
int setenv(const char *name, const char *value, int overwrite);
函数参数和返回值含义如下:
name:需要添加或修改的环境变量名称。
value:环境变量的值。
overwrite:若参数 name 标识的环境变量已经存在,在参数 overwrite 为 0 的情况下,setenv()函数将不改变现有环境变量的值,也就是说本次调用没有产生任何影响;如果参数 overwrite 的值为非 0,若参数 name 标识的环境变量已经存在,则覆盖,不存在则表示添加新的环境变量。
返回值:成功返回 0;失败将返回-1,并设置 errno。 setenv()函数为形如 name=value 的字符串分配一块内存缓冲区,并将参数 name 和参数 value 所指向的字符串复制到此缓冲区中,以此来创建一个新的环境变量,所以,由此可知,setenv()与 putenv()函数有两个区别:
1、putenv()函数并不会为 name=value 字符串分配内存;
2、 setenv()可通过参数overwrite控制是否需要修改现有变量的值而仅以添加变量为目的,显然putenv() 并不能进行控制。
unsetenv()函数
unsetenv()函数可以从环境变量表中移除参数 name 标识的环境变量,其函数原型如下所示:
#include <stdlib.h>
int unsetenv(const char *name);
清空环境变量
有时,需要清除环境变量表中的所有变量,然后再进行重建,可以通过将全局变量 environ 赋值为 NULL来清空所有变量。
environ = NULL;
也可通过 clearenv()函数来操作,函数原型如下所示:
#include <stdlib.h>
int clearenv(void);
clearenv()函数内部的做法其实就是将environ赋值为NULL。在某些情况下,使用setenv()函数和clearenv() 函数可能会导致程序内存泄漏,前文提到,setenv()函数会为环境变量分配一块内存缓冲区,随之称为进程的一部分;而调用 clearenv()函数时没有释放该缓冲区(clearenv()调用并不知晓该缓冲区的存在,故而也无法将其释放),反复调用者两个函数的程序,会不断产生内存泄漏。