当前位置:   article > 正文

【Linux】环境变量、进程替换、wait/waitpid

【Linux】环境变量、进程替换、wait/waitpid


一、环境变量

1. 查看环境变量的方法

1.1 env

可以查看当前进程的所有环境变量.
在这里插入图片描述

1.2 echo $环境变量名

可以查看指定的环境变量.
在这里插入图片描述

2. 在代码中使用环境变量的方法

2.1 命令行参数传参

通过设置 main 函数的第三个形参 char* envp[ ], 接收环境变量.

#include <iostream>
using namespace std;

int main(int argc, char* argv[], char* envp[])
{
	for (int i = 0; envp[i]; ++i)
	{	
		cout << envp[i] << endl;
	}   
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11

程序运行起来会是 bash 的子进程, 子进程会继承父进程的环境变量, envp 的最后一个位置会存储一个 NULL, 所以可以用作循环条件的判断.

2.2 environ变量

通过 extern 关键字引用外部变量, extern char** environ.

#include <iostream>
using namespace std;

extern char** environ;

int main()
{
	for (int i = 0; environ[i]; ++i)
	{	
		cout << environ[i] << endl;
	}   
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

environ 不包含在任何头文件中, 所以要使用 extern 引入.

2.3 getenv( )函数

通过 getenv( ) 可以获取指定的环境变量, 传递的参数是环境变量名.
头文件: #include <stdlib.h>
函数声明: char *getenv(const char *name);

#include <iostream>
#include <stdlib.h>
using namespace std;

int main()
{
	cout << getenv("USER") << endl;
	cout << getenv("HOME") << endl;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

运行结果:
在这里插入图片描述

3. export

在 Linux 中, 像 ls, pwd 这样的指令可以直接敲了就运行, 而我们编写的代码在生成了可执行文件后要通过 ./xxx 的方式才可以运行, 这是为什么呢? 本质都是一个程序, 跑起来变成进程, 原因是 ls, pwd 这种指令的路径添加到了环境变量 PATH 中, 所以在运行时系统在环境变量 PATH 中可以找到, 而我们自己编写的程序的路径不存在环境变量 PATH 中, 所以要指定路径.

自己编写的程序, 如果不加 ./ 直接跑起来是这样的:
在这里插入图片描述

那么试着把自己编写的程序的所在路径添加到环境变量 PATH 中, 看看可不可以不需要 ./ 直接跑起来, 通过 export 添加到环境变量:

export PATH=$PATH:/root/EnvTest/Test/
  • 1

$PATH 表示之前添加到 PATH 中的所有路径, 将自己编写的程序的所在路径写在冒号右边就好, 不要直接写成 PATH=/root/EnvTest/Test/, 这样写会把之前的路径都覆盖掉, 属于覆盖式写入.

运行结果:
在这里插入图片描述
可以看到可以不带 ./ 直接运行了, 再看看 PATH 中也存在了刚才添加的路径:
在这里插入图片描述

二、进程替换

用 fork 创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支), 子进程往往要调用一种 exec 函数以执行另一个程序。当进程调用一种exec函数时, 该进程的代码和数据完全被新程序替换, 从新程序的启动例程开始执行. 调用exec并不创建新进程,所以调用exec 前后该进程的 pid 不会改变.

下面介绍各种 exec 函数, 头文件均为: #include <unistd.h>

1. execl

函数声明: int execl(const char *path, const char *arg, …);

  • path: 替换程序的路径
  • arg/…: 可变参数列表
#include <iostream>
#include <unistd.h>
using namespace std;

int main()
{
	cout << "111" << endl;
	cout << "222" << endl;
	cout << "333" << endl;
	
	execl("/usr/bin/ls", "ls", "-l", "-a", NULL);
	
	cout << "444" << endl;
	cout << "555" << endl;
	cout << "666" << endl;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

假设要替换的进程为 ls, 那么就把 ls 的路径写在 path 处, 在可变参数列表分别加上在终端如何执行 ls 的指令, 每个选项单独写, 最后必须以 NULL 结尾.

运行结果:
在这里插入图片描述
可以看到前面三行代码正常输出, 进程替换后的代码不会输出, 因为在函数调用处后的代码会被替换.

2. execlp

函数声明: int execlp(const char *file, const char *arg, …);

  • file: 替换程序名
  • arg/…: 可变参数列表
#include <iostream>
#include <unistd.h>
using namespace std;

int main()
{
	cout << "111" << endl;
	cout << "222" << endl;
	cout << "333" << endl;
	
	execl("ls", "ls", "-l", "-a", NULL);
	
	cout << "444" << endl;
	cout << "555" << endl;
	cout << "666" << endl;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17

path 处不需要指明路径了, 直接写上替换程序的名称即可, 后面的可变参数列表处和 execl 一致.

运行结果:
在这里插入图片描述
需要注意的是, 如果替换的程序是你自己编写的, 那么需要把它所在的路径添加到 PATH 中, 否则不会替换.

3. execle

函数声明: int execle(const char *path, const char *arg, …, char * const envp[ ]);

  • path: 替换程序的路径
  • arg/…: 可变参数列表
  • envp: 环境变量列表

程序 myenv, 执行起来就是查看该进程的环境变量:
在这里插入图片描述
execle 示例代码:

#include <iostream>
#include <unistd.h>
using namespace std;

int main()
{
	cout << "111" << endl;
	cout << "222" << endl;
	cout << "333" << endl;
	
	char*const envp[] = {"MYENV=100", NULL};
	execle("/root/ExecTest/myenv", "myenv", NULL, envp);
	
	cout << "444" << endl;
	cout << "555" << endl;
	cout << "666" << endl;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

传递环境变量需要自定义一个数组进行传递, 在数组中依然要以 NULL 结尾, 但是此时来看一看运行结果:
在这里插入图片描述
确实是把传递的环境变量给输出出来了, 但是之前的环境变量被覆盖了, 没错 execle 传递环境变量就是覆盖式传入, 那么如何做到不覆盖进程原本的环境变量, 又把新的环境变量传入呢? 先看一个函数 putenv( ):

头文件: #include <stdlib.h>
函数声明: int putenv(char *string);

该函数的功能很简单, 就是哪个进程调用该函数, 就把传递的环境变量形参设置到哪个进程的环境变量中.

结合该函数就可以实现不覆盖进程原本的环境变量, 又把新的环境变量传入, 如下:

#include <iostream>
#include <unistd.h>
#include <stdlib.h>
using namespace std;

extern char** environ;

int main()
{
	cout << "111" << endl;
	cout << "222" << endl;
	cout << "333" << endl;
	
	putenv("MYENV=100");
	execle("/root/ExecTest/myenv", "myenv", NULL, environ);
	
	cout << "444" << endl;
	cout << "555" << endl;
	cout << "666" << endl;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21

其实也很简单, 就是先把新的环境变量导入当前进程, 再把 environ 当作形参传递就好了.

运行结果:
在这里插入图片描述
可以看到, 不仅输出了原本的环境变量, 而且新添加的也输出了, 那么就成功做到了不覆盖进程原本的环境变量, 又把新的环境变量传入.

4. execv

函数声明: int execv(const char *path, char *const argv[]);

  • path: 替换程序的路径
  • argv: 执行该程序的指令数组
#include <iostream>
#include <unistd.h>
using namespace std;

int main()
{
	cout << "111" << endl;
	cout << "222" << endl;
	cout << "333" << endl;
	
	char*const argv[] = {"ls", "-l", "-a", NULL};
	execv("/usr/bin/ls", argv);
	
	cout << "444" << endl;
	cout << "555" << endl;
	cout << "666" << endl;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18

其实很简单, 和 execl 没什么区别, 无非是把替换程序的执行指令写在一个数组中, 再把数组作为参数传给 execv, 注意数组最后必须以 NULL 结尾即可.

运行结果:
在这里插入图片描述

5. execvp

函数声明: int execvp(const char *file, char *const argv[]);

  • file: 替换程序名
  • argv: 执行该程序的指令数组
#include <iostream>
#include <unistd.h>
using namespace std;

extern char** environ;

int main()
{
	cout << "111" << endl;
	cout << "222" << endl;
	cout << "333" << endl;
	
	char*const argv[] = {"ls", "-l", "-a", NULL};
	execv("ls", argv);
	
	cout << "444" << endl;
	cout << "555" << endl;
	cout << "666" << endl;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

和 execv 差不多, 无非是函数的第一个参数直接传可执行程序名即可.

运行结果:
在这里插入图片描述

6. execvpe

函数声明: int execvpe(const char *file, char *const argv[], char *const envp[]);

  • file: 替换程序名
  • argv: 执行该程序的指令数组
  • envp: 环境变量列表
#include <iostream>
#include <unistd.h>
#include <stdlib.h>
using namespace std;

extern char** environ;

int main()
{
	cout << "111" << endl;
	cout << "222" << endl;
	cout << "333" << endl;
	
	putenv("MYENV=100");
	char*const argv[] = {"myenv", NULL}; 
	execvpe("myenv", argv, environ);

	cout << "444" << endl;
	cout << "555" << endl;
	cout << "666" << endl;
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

无非就是第一个参数直接传递替换程序名, 第二个参数传递执行该程序的指令数组, 第三个参数传递环境变量数组, 和之前的大同小异
运行结果:
在这里插入图片描述

7. 补充

7.1 命名理解

exec 函数名中的规律:

  • 带 l 的: l = list, 表示传参直接传在函数参数列表中.
  • 带 v 的: v = vector, 表示一部分参数可以先写在数组中, 再把数组当作参数传递.
  • 带 p 的: p = path, 表示不需要传路径, 直接传递替换程序名即可.
  • 带 e 的: e = env, 表示自己维护环境变量.

7.2 返回值

exec( ) 函数仅在发生错误时返回, 返回值为 -1,并设置 errno 以指示错误, 其实不需要关心返回值, 因为如果成功程序会被替换, 失败不会被替换, 剩于的代码也会执行, 无论成功与否都可以很直观的判断出函数执行的结果.

三、wait/waitpid

子进程在退出后会变成僵尸进程, 通过 wait/waitpid 可以再获取了子进程的退出状态后将身为僵尸进程的子进程释放, 如果长期不释放会导致内存泄漏.

头文件均为:
#include <sys/types.h>
#include <sys/wait.h>

1. wait

函数原型: pid_t wait(int *status);
参数: 输出型参数, 可以获取进程的退出状态, 不需要获取退出状态设置为NULL.
返回值:

  • 成功: 返回被等待的进程的 pid.
  • 失败: 返回 -1, 这时errno会被设置成相应的值以指示错误所在.

示例代码:

#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;

int main()
{
	pid_t ret1 = fork(); //以此往后分化为两个进程执行
	//子进程
	if (ret1 == 0)
	{
		for (int i = 0; i < 5; ++i)
		{
			cout << "I am child, my pid:" << getpid() << endl;
			sleep(1);
		}
	}
	else
	{
		pid_t ret2 = wait(NULL);
		cout << "wait success, ret2:" << ret2 << endl;
	}
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

子进程调用 5 次 cout << “I am child, my pid:” << getpid() << endl; 输出信息, 最后由父进程成功 wait 并且输出 wait 的返回值.

运行结果:
在这里插入图片描述
通过图片无法看出, 但是以上代码是两个进程再跑, 但是情况是这样的, 父进程一直 wait 子进程退出, 而这种等待是阻塞式等待, 所以其结论就是父进程在使用 wait 时是处于阻塞式等待的, 需要子进程退出后并且父进程 wait 结束后才会往下执行父进程剩于的代码.

2. waitpid

函数原型: pid_t waitpid(pid_t pid, int *status, int options);
参数:

  • pid: 设置为 -1 表示等待任一子进程, 设置为 >0 表示等待指定 pid 的子进程.
  • status: 输出型参数, 可以获取进程的退出状态, 不需要获取退出状态设置为NULL.
  • options: 设置为 0 表示阻塞式等待, 设置为 WNOHANG 表示非阻塞式等待.

返回值:

  • 正常返回时, 返回等待的子进程的 pid.
  • 如果 options 设置了选项WNOHANG, 而调用中 waitpid 发现没有已退出的子进程可等待, 则返回0.
  • 失败返回 -1, 这时errno会被设置成相应的值以指示错误所在.

示例代码:

#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;

int main()
{
	pid_t ret1 = fork(); //以此往后分化为两个进程执行
	//子进程
	if (ret1 == 0)
	{
		for (int i = 0; i < 5; ++i)
		{
			cout << "I am child, my pid:" << getpid() << endl;
			sleep(1);
		}
	}
	else
	{
		int status = 0;
		pid_t ret2 = 0;
		do
		{
			ret2 = waitpid(ret1, &status, WNOHANG);
			cout << "father is running." << endl;
			sleep(1);
		}while(ret2 == 0)
		cout << "waitpid success, ret2:" << ret2 << endl;
	}
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

运行结果:
在这里插入图片描述
可以看到在子进程没有退出时, 父进程还是有在往后执行的, 而且在子进程退出时, 父进程也能成功的等待到子进程.

3. 获取子进程的退出状态

在 wait/waitpid 中都有 status, status 是 int 类型, 那么共有 32 位, status 是当作一个位图使用, 在其中存储着退出状态信息, 如下图:
在这里插入图片描述
其中 status 的高 16 位不用, 只使用低 16 位, 而次低 8 位用于存储退出码, 就是 return, exit 携带的数字, 低 7 位存储终止信号, 如果终止信号为 0 表示正常退出, 接着可以获取其退出码, 判断结果是否正确, 如果终止信号不为 0, 表示进程是被信号所终止退出的, 此时为异常退出, 异常退出退出码没有意义, 所以未被设置, 直接获取终止信号判断分析异常退出原因即可.

3.1 位运算获取退出状态

获取退出码: ((status >> 8) & (0xff))
获取终止信号: (status & (0x7f))

退出码示例代码:

#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;

int main()
{
	pid_t ret1 = fork(); //以此往后分化为两个进程执行
	//子进程
	if (ret1 == 0)
	{
		for (int i = 0; i < 5; ++i)
		{
			cout << "I am child, my pid:" << getpid() << endl;
			sleep(1);
		}
	}
	else
	{
		int status = 0;
		pid_t ret2 = 0;
		do
		{
			ret2 = waitpid(ret1, &status, WNOHANG);
			cout << "father is running." << endl;
			sleep(1);
		}while(ret2 == 0)
		cout << "waitpid success, ret2:" << ret2 << ", exit code:" << ((status >> 8) & (0xff)) << ", signal:" << (status & (0x7f)) << endl;
	}
	return 0;
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

运行结果:
在这里插入图片描述
为了直观的看到退出码, 改变一下代码:

#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;

int main()
{
	pid_t ret1 = fork(); //以此往后分化为两个进程执行
	//子进程
	if (ret1 == 0)
	{
		for (int i = 0; i < 5; ++i)
		{
			cout << "I am child, my pid:" << getpid() << endl;
			sleep(1);
		}
		return 111; //为了更直观的看到退出码
	}
	else
	{
		int status = 0;
		pid_t ret2 = 0;
		do
		{
			ret2 = waitpid(ret1, &status, WNOHANG);
			cout << "father is running." << endl;
			sleep(1);
		}while(ret2 == 0)
		cout << "waitpid success, ret2:" << ret2 << ", exit code:" << ((status >> 8) & (0xff)) << ", signal:" << (status & (0x7f)) << endl;
	}
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33

运行结果:
在这里插入图片描述

终止信号示例代码:

#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;

int main()
{
	pid_t ret1 = fork(); //以此往后分化为两个进程执行
	//子进程
	if (ret1 == 0)
	{
		for (int i = 0; i < 5; ++i)
		{
			cout << "I am child, my pid:" << getpid() << endl;
			sleep(1);
		}
		return 111; //为了更直观的看到退出码
	}
	else
	{
		int status = 0;
		pid_t ret2 = 0;
		do
		{
			ret2 = waitpid(ret1, &status, WNOHANG);
			cout << "father is running." << endl;
			sleep(1);
		}while(ret2 == 0)
		if((status & (0x7f)))
		{
			cout << "waitpid success, ret2:" << ret2 << ", signal:" << (status & (0x7f)) << endl;                                                                       
		} 
	}
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36

如果终止信号不为 0, 表示异常退出, 此时就没必要获取退出码了, 因为此时的退出码不具有参考性, 没有获取的必要.

发送 9 号信号 kill 掉进程:

kill -9 20266
  • 1

运行结果:
在这里插入图片描述

3.2 内置宏获取退出状态

WIFEXITED(status): 若为正常终止子进程返回的状态, 则为真.(查看进程是否是正常退出)
WEXITSTATUS(status): 若WIFEXITED非零, 提取子进程退出码.(查看进程的退出码)

示例代码:

#include <iostream>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
using namespace std;

int main()
{
	pid_t ret1 = fork(); //以此往后分化为两个进程执行
	//子进程
	if (ret1 == 0)
	{
		for (int i = 0; i < 5; ++i)
		{
			cout << "I am child, my pid:" << getpid() << endl;
			sleep(1);
		}
		return 111; //为了更直观的看到退出码
	}
	else
	{
		int status = 0;
		pid_t ret2 = 0;
		do
		{
			ret2 = waitpid(ret1, &status, WNOHANG);
			cout << "father is running." << endl;
			sleep(1);
		}while(ret2 == 0)
		//正常退出,查看退出码
		if(WIFEXITED(status))
		{
			cout << "waitpid success, ret2:" << ret2 << ", exit code:" << WEXITSTATUS(status) << endl;
		}
		else
		{
			cout << "waitpid success, ret2:" << ret2 << ", signal:" << (status & (0x7f)) << endl;
		}
	}
	return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

因为没有提供查看终止信号的宏, 所以查看终止信号仍需要用位运算的方法.

运行结果:
在这里插入图片描述

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/534135
推荐阅读
相关标签
  

闽ICP备14008679号