前言:本篇博客是接着我的上一遍博客:进程深入学习笔记(1)继续对进程一些相关概念的学习理解。
进程深入学习笔记
1.环境变量
(1)基本概念
环境变量一般是指在操作系统中用来指定操作系统运行环境的一些参数,比如临时文件夹位置和系统文件夹位置等等。环境变量通常具有某些特殊用途,还有在系统中通常具有全局特性。例如:在执行代码的时候,我们不知道所链接的库在哪里,但是依然可以执行成功,原因是因为相关的环境变量帮助编译器在查找。
(2)常见的环境变量
-
PATH:指定命令的搜索路径 -
HOME:指定用户的主工作目录(既登录到linux系统默认的目录) -
HISTSIZE:保存历史命令记录的条数 -
SHELL:当前的Shell,它的值通常是/bin/bash
(3)查看环境变量
echo $NAME //NAME是指你的环境变量名称,例如echo $PATH
(4)用例子说明PATH
按变量的生存周期来划分,Linux环境变量可分为两类:
- 永久的:需要修改配置文件,变量
永久生效。 - 临时的:使用
export命令声明即可,变量在关闭shell时失效。
假设我们写一个test.c的程序并编译生成test可执行程序。通常,我们运行可执行程序的时候要./test(./代表当前目录),既先要加上路径后边跟上文件才能执行。但是我们可以发现一些命令,比如ls不用加目录就可以直接执行,因为它的路径在PATH这个环境变量中。我们可以试着把test的路径放到环境变量中,然后直接输入它的名字,它也同样可以运行。
export PATH=$PATH:test文件所在的路径
将test的的路径加入到PATH中,我们可以发现到哪里输入test都可以随意运行这个文件。
(5)相关命令
-
echo:显示某个环境变量的值 -
export:设置一个新的环境变量 -
env:显示所有的环境变量 -
unset:清除环境变量 -
set:设置本地定义的shell变量和环境变量
(5)环境变量的组织方式
每个程序都有一张环境表,环境表示一个字符指针数组,每个指针指向一个以\0结尾的环境字符串,数组最后一个元素是 NULL
(6)getenv、putenv
getenv:
- 函数:
char * getenv(const char *name) - 头文件:
#include<stdlib.h> - 作用:用来获取
name环境变量的内容 - 参数说明:
name为环境变量的名称,如果该变量存在则会返回指向该内容的指针(环境变量的格式为name=value),不存在则返回NULL
putenv:
- 函数:
int putenv(const char * string) - 头文件:
#include<stdlib.h> - 作用:用来
改变或增加环境变量的内容 - 参数说明:
string的格式为name=value,如果该环境变量原先存在,则变量内容会依参数string改变,否则此参数内容会成为新的环境变量 - 返回值:执行
成功则返回0,错误发生则返回-1
(7)获取环境变量的方法
- 通过命令行的第三个参数
#include<stdio.h>
int main(int argc,char* argv[],char* env[])
{
int i = 0;
for( ;env[i];i++)
{
printf("%s\n",env[i]);
}
return 0;
}
- 通过第三方变量environ获取
#include<stdio.h>
int main(int argc,char* argv[])
{
extern char** environ;
//全局变量environ是一个指向环境变量表的全局变量,没有包含在任何头文件,要用extern声明
int i = 0;
for( ;environ[i];i++)
{
printf("%s\n",environ[i]);
}
return 0;
}
- 通过
getenv系统调用获取
#include<stdio.h>
#include<stdlib.h>
int main()
{
printf("%s\n",getenv("PATH"));
return 0;
}
(7)环境变量的特性
- 环境变量具有全局的特性
- 它可以被子进程继承
#include<stdio.h>
#include<stdlib.h>
int main()
{
char *env = getenv("MYENV");
if(env)
{
printf("%s\n",env);
}
return 0;
}
程序运行后,发现没有这个环境变量,但是执行命令export MYENV="hello",再次运行程序可以得到这个环境变量,说明环境变量可以被子进程继承。export是把“变量”放到“环境”中,而环境变量是可以被子进程继承的。这里需要区分的就是本地变量和环境变量,前者不能被子进程继承,后者则可以。通过export就是把本地变量设置成环境变量
注:想要深入了解这块的内容,可以参考《UNIX环境高级编程》第7章进程环境来学习
2.进程地址空间
(1)一个例子说明地址空间
在学习C语言的时候,我们见过这样一幅空间布局图,但是这幅图描述的是物理内存吗?答案是否定的,因为如果这就是物理内存的话,那么一个程序就把4G内存给用完了,其它的程序还怎么用呀。事实上,这是进程的虚拟地址空间,是用来描述一个空间的。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int g_val=0;
int main(){
pid_t id=fork();
if(id<0){
perror("use fork");
exit(1);
}
else if(id==0){//child
g_val=100;
printf("I am child:[%d],%d,%p\n",getpid(),g_val,&g_val);
}
else{//parent
printf("I am parent:[%d],%d,%p\n",getpid(),g_val,&g_val);
}
sleep(1);
return 0;
}
输出结果为:
由上边的输出结果我们可以得出:
-
父子进程输出的变量不是同一个变量,但是地址是一样的,说明这个地址不是物理地址。 - 在Linux操作系统下,这样的地址叫
虚拟地址,我们用C/C++语言看到的所有地址都是虚拟地址,物理地址由操作系统统一管理。
(2)一张图说明虚拟地址空间
上边的这张图可以说明:
- 同一个变量,地址相同指的是
虚拟地址相同,它们通过页表被映射到了不同的物理地址。 - 每一个进程都有一个
PCB、虚拟地址空间、页表和维护映射关系。 - 当父进程创建一个子进程时,如果
只读的话,父子进程的代码和数据是共享的,也就是说它们的虚拟地址空间和页表都是共享的,但是如果有一方试图修改数据,父子进程就以写时copy的方式数据各私有一份,既虚拟地址空间和页表不同。
注:虚拟内存的知识学习可以参考《深入理解计算机系统》第九章虚拟内存章节
(3)虚拟地址空间的作用
-
保护物理内存(例如:可以避免野指针) 安全-
资源独占(32位平台下每个虚拟地址空间都是4G,每个进程都可以认为它有4G的空间
3.进程终止(exit&_exit)
(1)进程退出的三种场景
- 代码运行完毕,结果
正确 - 代码运行完毕,结果
不正确 - 代码
异常终止
(2)进程常见退出方法(exit&_exit)
正常退出:
-
main函数返回 - 调用
exit
#include<unistd.h>
void exit(int status);
//status定义进程的终止状态,父进程通过wait获得其值
- 调用
_exit
#include<unistd.h>
void _exit(int status);
//status定义进程的终止状态,父进程通过wait获得其值(0异常返回,1正常返回)
注:虽然status是int整型,但是只有低8位可被父进程所用。当_exit(-1),终端执行命令echo $?可以发现返回值是255
exit和_exit的区别:
-
exit会刷新缓冲区,关闭流,而_exit不刷新缓冲区。
异常退出:
-
Ctrl+c信号终止 -
kill -9 pid向pid进程发9号信号
(2)return退出
return退出是一种常见的进程退出方法。return m等同于exit(m),因为调用main的运行时函数会把main的返回值当做exit的参数。
4.进程等待(wait&waitpid)
(1)为什么存在进程等待?
- 当
子进程退出,父进程没有接受子进程的退出状态,就会产生"僵尸状态",子进程一旦变为"僵尸状态",不能被kill,所以会造成内存泄露的问题 - 父进程希望知道子进程的一些信息,如
否运行完成,结果是否正确等等 - 所以父进程通过等待的方式:
回收子进程资源(避免内存泄露);获取子进程退出信息(退出码)
(2)进程等待的方法
a.wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int status);
//输出型参数,用来获取子进程退出状态,不关心可以设置为NULL
- 返回值:
成功返回被等待子进程的pid,失败返回-1
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
注:有两个或者两个以上的子进程,wait只对其中任意一个子进程起作用
b.waitpid方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);
参数:
- pid:
要等待子进程的pid
pid>0时,只等待进程ID等于pid的子进程,不管其它已经有多少子进程运行结束退出了,只要指定的子进程还没有结束,waitpid就会一直等下去。pid=-1时,等待任何一个子进程退出,没有任何限制,waitpid和wait的作用一样。pid=0时,等待同一个进程组中的任何子进程,如果子进程已经加入了别的进程组,waitpid不会对它做任何理睬。pid<-1时,等待一个指定进程组中的任何子进程,这个进程组的ID等于pid的绝对值。
- status:如果参数status的值
不是NULL,waitpid就会把子进程退出时的状态取出并存入其中,这是一个整数值(int),指出了子进程是正常退出还是非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个:
WIFEXITED(status)这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值(虽然名字一样,这里的参数status并不同于waitpid的参数指向整数的指针status,而是那个指针所指向的整数)WEXITSTATUS(status)当WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的退出码,如果子进程调用exit(25)退出,WEXITSTATUS(status)就会返回25。如果进程不是正常退出的,WIFEXITED返回0,这个值就无意义。
- options:
options提供了一些额外的选项来控制waitpid,目前在Linux中支持WNOHANG(非阻塞式 1)和WUNTRACED(阻塞式 0)两个选项,这是两个常数,可以用"|"运算符把它们连接起来使用,例如:waitpid(-1,NULL,WNOHANG | WUNTRACED);如果我们不想使用它们,也可以把options设为0,例如:·waitpid(-1,NULL,0)
使用WNOHANG参数调用waitpid,即使没有子进程退出,它也会立即返回,不会像wait那样永远等下去。WUNTRACED参数,涉及到一些跟踪调试方面的知识,加之极少用到,有兴趣的读者可以自行查阅相关材料。
(3)获取子进程的status
-
wait和waitpid都要一个输出型参数status,用来获取被等待进程的退出状态 - 如果设置为
NULL,则表示不关心子进程的退出状态信息,否则,操作系统或根据该参数,将子进程的退出信息反馈给父进程 - status不能当做简单的整型看待,只有它的
低16位被用作status
由上图可以看出:
-
正常终止时,status的次低8位(8-15)代表退出码,而0-67位位0。 -
被信号杀死时,status的低7位为退出码。
a.wait测试代码
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
pid_t pid=fork();
if(pid==-1){
perror("use fork");
exit(1);
}
else if(pid==0){//child
sleep(20);
exit(10);
}
else{//parent
int st;//status
int ret=wait(&st);
if(ret>0&&(st&0X7F)==0)//normal exit
printf("child exit code is [%d]\n",(st>>8)&0XFF);
//退出码是次低8位
else//abnormal exit
printf("sign code is[%d]\n",st&0X7F);
//退出码是0-7位
}
}
下边是运行结果:
[[email protected] test_procwait]$ ./test_procwait
child exit code is [10]//正常退出
[[email protected] test_procwait]$ ./test_procwait
sign code is[9]//在另一个终端将子进程杀死
b.waitpid阻塞式测试代码
#include<stdio.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<unistd.h>
int main(){
pid_t id=fork();
if(id<0){
perror("use fork");
exit(1);
}
else if(id==0){//child
printf("child is run,pid is [%d]\n",getpid());
sleep(5);
exit(37);
}
else{//parent
int st;//status
pid_t ret=waitpid(-1,&st,0);//0代表阻塞式
printf("this is test for waitpid\n");
if(WIFEXITED(st)&&ret==id)//WIFEXITED判断是否正常返回
printf("wait child 5s success,child return code is [%d]\n",WEXITSTATUS(st));
//WEXITSTATUS宏获取退出码
else{
printf("wait child failed,return.\n");
return 1;
}
}
return 0;
}
下边是运行结果:
[[email protected] test_procwaitpid]$ ./test_procwaitpid
child is run,pid is [5380]
this is test for waitpid
wait child 5s success,child return code is [37]
c.waitpid非阻塞式测试代码
#include<stdio.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<unistd.h>
int main(){
pid_t id=fork();
if(id<0){
perror("use fork");
exit(1);
}
else if(id==0){//child
printf("child is run,pid is [%d]\n",getpid());
sleep(5);
exit(1);
}
else{//parent
int st;//status
pid_t ret=0;
do{
ret=waitpid(-1,&st,WNOHANG);//WNOHANG是1,代表非阻塞式等待
if(ret==0){
printf("child is running\n");
}
sleep(1);
}while(ret==0);
if(WIFEXITED(st)&&ret==id)
printf("wait child 5s success,child return code is [%d]\n",WEXITSTATUS(st));
else
printf("wait child failed,return.\n");
return 1;
}
return 0;
}
下边是运行结果:
[[email protected] test_procwaitpid]$ ./test_procwaitpid
child is running
child is run,pid is [5803]
child is running
child is running
child is running
child is running
wait child 5s success,child return code is [1]