【Linux系统编程】环境变量,进程地址空间与进程控制
目录
一,环境变量
1,基本概念
2,常见环境变量
3,查看环境变量的方法
4,和环境变量相关的命令
5,环境变量的组织方式
6,通过系统调用获取和设置环境变量
7,环境变量通常具有全局性
二,进程地址空间/虚拟地址空间
三,进程控制
1,进程创建
2,进程终止
3,进程等待
进程等待必要性
进程等待的⽅法
获取子进程status
4,进程程序替换
替换原理
替换函数
一,环境变量
1,基本概念
环境变量(environment variables)⼀般是指在操作系统中⽤来指定操作系统运行环境的⼀些参数。
-
如:我们在编写C/C++代码的时候,在链接的时候,从来不知道我们的所链接的动态静态库在哪⾥,但是照样可以链接成功,⽣成可执⾏程序,原因就是有相关环境变量帮助编译器进⾏查找。
-
环境变量通常具有某些特殊⽤途,还有在系统当中通常具有全局特性。
2,常见环境变量
-
PATH : 指定命令的搜索路径
-
HOME : 指定用户的主工作目录(即用户登录到Linux系统中时,默认的⽬录)
-
SHELL : 当前Shell,它的值通常是/bin/bash。
3,查看环境变量的方法
echo $NAME //NAME:你的环境变量名称
4,和环境变量相关的命令
-
echo: 显⽰某个环境变量值
-
export: 设置⼀个新的环境变量
-
env: 显⽰所有环境变量
-
unset: 清除环境变量
-
set: 显⽰本地定义的shell变量和环境变量
5,环境变量的组织方式
每个程序都会收到⼀张环境变量表,环境变量表是⼀个字符指针数组,每个指针指向⼀个以’\0’结尾的字符串。
6,通过系统调用获取和设置环境变量
常⽤getenv
和putenv
函数来访问特定的环境变量。
#include <stdio.h>
#include <stdlib.h>
int main()
{
printf("%s\n", getenv("PATH"));
return 0;
}
7,环境变量通常具有全局性
环境变量通常具有全局属性,可以被子进程继承下去。
二,进程地址空间/虚拟地址空间
分页&虚拟地址空间
缺页中断:使用虚拟地址通过页表查看物理地址,虚拟地址合法,但是物理地址找不到,操作系统此时会将代码和数据加载到内存中,并填充页表,进程就可以继续运行了。
写时拷贝:父子进程将数据部分的权限设置为只读,当某一个进行写的时候,操作系统就会识别出需要进行写时拷贝。
野指针:通过虚拟地址查找物理地址时,找不到。
其中mm_struct中还包含struct vm_area_struct*的链表,来管理堆空间(因为堆可能有多个),或者管理其他空间。
三,进程控制
1,进程创建
在 linux 中 fork 函数是非常重要的函数,它从已存在进程中创建⼀个新进程。新进程为子进程,而原进程为父进程。
include <unistd.h>
pid_t fork(void);
//返回值:⼦进程中返回0,⽗进程返回⼦进程id,出错返回-1
进程调用 fork ,当控制转移到内核中的 fork 代码后,内核做:
-
分配新的内存块和内核数据结构给子进程
-
将父进程部分数据结构内容拷贝至子进程
-
添加子进程到系统进程列表当中
-
fork 返回,开始调度器调度
2,进程终止
进程终止的本质是释放系统资源,就是释放进程申请的相关内核数据结构和对应的数据和代码。
进程退出场景
-
代码运行完毕,结果正确
-
代码运行完毕,结果不正确
-
代码异常终止
进程常见退出方法
正常终止(可以通过 echo $? 查看进程退出码):
-
从main返回
-
调⽤exit
-
_exit(系统调用,不会进行缓冲区的刷新)
异常退出:
-
ctrl + c,信号终止
退出码(退出状态)可以告诉我们最后⼀次执⾏的命令的状态。在命令结束以后,我们可以知道命令是成功完成的还是以错误结束的。其基本思想是,程序返回退出代码 0 时表示执行成功,没有问题。代码 1 或 0 以外的任何代码都被视为不成功。
-
退出码 0 表⽰命令执行无误,这是完成命令的理想状态。
-
退出码 1 我们也可以将其解释为 “不被允许的操作”。例如在没有 sudo 权限的情况下使用yum;再例如除以 0 等操作也会返回错误码 1 ,对应的命令为 let a=1/0
-
130 ( SIGINT 或 ^C )和 143 ( SIGTERM )等终⽌信号是⾮常典型的,它们属于128+n 信号,其中 n 代表终止码。
-
可以使⽤ strerror 函数来获取退出码对应的描述。
3,进程等待
进程等待必要性
-
子进程退出,父进程如果不管不顾,就可能造成‘僵⼫进程’的问题,进而造成内存泄漏。
-
另外,进程一旦变成僵尸状态,那就刀枪不入,“杀⼈不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死⼀个已经死去的进程。
-
最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
-
父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。
进程等待的⽅法
wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
返回值:
成功返回被等待进程pid,失败返回-1。
参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);
返回值:当正常返回的时候waitpid返回收集到的⼦进程的进程ID;如果设置了选项WNOHANG,⽽调⽤中waitpid发现没有已退出的⼦进程可收集,则返回0;如果调⽤中出错,则返回-1,这时errno会被设置成相应的值以指示错误所在;
参数:Pid=-1,等待任⼀个⼦进程。与wait等效。Pid>0.等待其进程ID与pid相等的⼦进程。status: 输出型参数WIFEXITED(status): 若为正常终⽌⼦进程返回的状态,则为真。(查看进程是否是正常退出)WEXITSTATUS(status): 若WIFEXITED⾮零,提取⼦进程退出码。(查看进程的退出码)options:默认为0,表⽰阻塞等待WNOHANG: 若pid指定的⼦进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该⼦进程的ID。
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即即返回,并且释放资源,获得子进程退出信息。
-
如果在任意时刻调用wait/waitpid,子进程存在且正常运⾏,则进程可能阻塞。
-
如果不存在该子进程,则立即出错返回。
获取子进程status
-
wait和waitpid,都有⼀个status参数,该参数是一个输出型参数,由操作系统填充。
-
如果传递NULL,表示例不关心子进程的退出状态信息。
-
否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位):
4,进程程序替换
在进程替换的时候,只需要将要执行的程序的代码和数据覆盖式的加载到内存中对应位置即可,进程数据结构对象和虚拟内存不需要修改,页表可能需要进行修改。
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec 函数以执行另一个程序。当进程调用一种 exec 函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用 exec 并不创建新进程,所以调用 exec 前后该进程的 id 并未改变。
替换函数
#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execve(const char *path, char *const argv[], char *const envp[]);
-
这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回。
-
如果调⽤出错则返回 -1。
-
所以 exec 函数只有出错的返回值⽽没有成功的返回值。
这些函数原型看起来很容易混,但只要掌握了规律就很好记。
-
l(list) : 表示参数采用列表
-
v(vector) : 参数用数组
-
p(path) : 有 p 自动搜索环境变量 PATH
-
e(env) : 表示自己维护环境变量
exec调用举例如下:
#include <unistd.h>
int main()
{char *const argv[] = {"ps", "-ef", NULL};char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};execl("/bin/ps", "ps", "-ef", NULL);// 带p的,可以使⽤环境变量PATH,⽆需写全路径execlp("ps", "ps", "-ef", NULL);// 带e的,需要⾃⼰组装环境变量execle("ps", "ps", "-ef", NULL, envp);execv("/bin/ps", argv);// 带p的,可以使⽤环境变量PATH,⽆需写全路径execvp("ps", argv);// 带e的,需要⾃⼰组装环境变量execve("/bin/ps", argv, envp);exit(0);
}
事实上,只有 execve 是真正的系统调用,其它五个函数最终都调用execve ,所以execve 在man手册第2节,其它函数在 man 手册第3节。这些函数之间的关系如下图所示。