再谈Linux 进程:进程等待、进程替换与环境变量
目录
1.进程等待
为什么需要进程等待?
相关系统调用:wait()和waitpid()
wait():
waitpid():
解析子进程状态(status)
2.进程替换
为什么需要进程替换?
相关系统调用:exec函数家族
3.环境变量
编辑
编辑
本文将对Linux环境下的进程:包括进程创建、终止与进程等待、替换进行讲解,作者使用XShell连接配置为CentOs 7.6的主机进行演示,希望能帮助你更好理解操作系统的运行原理!
1.进程等待
在操作系统中,进程等待是指父进程主动等待子进程结束执行,并回收子进程资源的过程。这是进程管理中的重要机制,主要用于解决子进程结束后资源释放和状态获取的问题。
为什么需要进程等待?
-
回收子进程资源
子进程结束后,其内核数据结构PCB不会立即释放,会变成僵尸进程。(ps:僵尸进程不能被杀死,只能通过进程等待解决!)父进程通过等待机制回收子进程的残留资源,避免内存泄漏和系统资源浪费。 -
获取子进程退出状态
父进程可以通过等待机制获取子进程的执行结果(退出的状态值、被终止收到的信号),进行后续处理。
相关系统调用:wait()和waitpid()
先来看看man-pages的wait介绍:
wait():
- 作用:阻塞当前父进程,直到任意一个子进程结束或被信号中断。
- 参数:
status是一个指向整数的指针,用于存储子进程的退出状态(可通过宏解析)。 - 返回值:
- 成功时返回结束的子进程的PID;
- 失败时返回 -1。
//以下是对进程等待的测试
int testwait()
{pid_t id=fork();if(id<0){perror("fork");return 1;}else if(id==0){//childint cnt=5;while(cnt){printf("I am child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);cnt--;sleep(1);}exit(0);}else {//parentint cnt=10;while(cnt){printf("I am father,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);cnt--;sleep(1);}//这里先不介绍status
-----------------------------------------------------------------------------------------pid_t ret=wait(NULL);//这里wait返回的就是子进程创建成功后返回的pidif(ret==id){printf("success!\n");}
-----------------------------------------------------------------------------------------}return 0;
}//补充:如果创建了多个子进程,又如何通过wait获得子进程的运行状态呢?
//在---画出的区域可以这样修改:
//对于多个子进程,只需要将wait遍历即可for(i=0;i<n;i++){pid_t ret=wait(NULL);if(ret>0){printf("wait %d success\n",ret);}}
waitpid():
- 作用:等待指定的子进程(更灵活,可设置非阻塞等待)。
- 参数:
pid
:- pid = -1:等待任意子进程;
- pid > 0:等待 PID 为 pid 的子进程(一般设定为指定等待子进程的PID);
status
:同上,用于获取子进程状态。options
:- 0(默认选择):阻塞模式,若子进程未结束则一直等待(阻塞状态);
- WNOHANG: 非阻塞模式,若子进程未结束则立即返回0。
- 返回值:
- 子进程未结束:根据options返回 0(非阻塞)或阻塞等待;
- 子进程结束:返回该子进程的 PID;
- 出错:返回 -1。
int testwaitplus()
{pid_t id=fork();if(id<0){perror("fork");return 1;}else if(id==0){//childint cnt=20;while(cnt){printf("I am child,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);cnt--;sleep(1);}exit(0);}else {//parentint cnt=10;while(cnt){printf("I am father,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt);cnt--;sleep(1);}//pid_t ret=wait(NULL);//这里wait返回的就是子进程创建成功后返回的pidwhile(1){int status=0;// pid_t ret=waitpid(-1,&status,0);pid_t ret=waitpid(-1,&status,WNOHANG);if(ret>0){printf("success!\n");printf("exit successfully,pid:%d\n,exit_code:%d,exit_signal:%d\n",ret,(status>>8)&0xFF,status&0x7F);//还可以用宏if(WIFEXITED(status))//判断是否异常退出{printf("进程正常退出,无异常!exit_code:%d\n",WEXITSTATUS(status));}exit(0);}else if(ret<0){printf("wait failed\n");}else {printf("子进程还未退出,waiting...\n");}sleep(1);}}return 0;
}
解析子进程状态(status)
这里的status到底是什么?这里详细介绍一下这个玩意:
status本质上就是一个整形:4个字节对应32个bit位:
0000 0000 0000 0000 0000 0000 0000 0000
前16位不考虑,后十六位进行写入标记;
0000 0000 0000 0000后七位表示异常信号,倒数第八位(标红)标识core dump,前八位表示退出码,自己想要查看两码可以进行位操作,也可以用宏:
WIFEXITED(status): 若为正常终止子进程返回的状态,则为真
WEXITSTATUS(status): 若WIFEXITED非零,提取子进程退出码其实本质上就是进行的位操作:
(status>>8)&0xFF(0xFF十六进制)
status&0x7F(0x7F十六进制)
2.进程替换
在操作系统中,进程替换是指用一个新的程序(可执行文件或脚本)替换当前进程的内存空间、代码和数据,使进程转而执行新程序的过程。这一过程不会创建新进程,而是直接覆盖当前进程的上下文,因此进程的PID保持不变,但执行的内容被完全替换。
为什么需要进程替换?
- 执行外部程序
例如,在Shell中输入ls命令时,Shell会通过进程替换让当前子进程执行ls程序。 - 程序升级或切换功能
一个进程在运行中需要切换到另一个功能模块时,可通过替换自身来实现。
相关系统调用:exec函数家族
进程替换通过exec系列函数实现,共有 7个函数,统称为exec函数族。它们的作用是加载并执行一个新程序,替换当前进程的内存空间。
函数原型与区别:
上述六个都是库函数,都是基于系统调用execve实现的:
给出总结:
函数名 | 参数形式 | 是否搜索 PATH 环境变量 | 能否自定义环境变量 |
---|---|---|---|
execl | 可变参数列表(以 NULL 结尾) | 否 | 否 |
execlp | 可变参数列表 | 是(根据 PATH 查找程序) | 否 |
execle | 可变参数列表 | 否 | 是(传入环境变量数组) |
execv | 字符指针数组(argv ) | 否 | 否 |
execvp | 字符指针数组 | 是 | 否 |
execvpe | 字符指针数组 | 是 | 是 |
execve | 字符指针数组 | 否 | 是(系统调用) |
核心区别:
参数传递方式:带 I 的函数(类似链表)通过逗号分隔的可变参数列表传递参数,最后以NULL结尾;带 v 的函数(类似数组)通过字符指针数组传递参数,类似main函数的参数形式。
是否搜索 PATH
:带 p 的函数会根据系统环境变量PATH查找程序路径,无需指定绝对路径。
环境变量控制:带 e 的函数可以自定义环境变量,否则继承当前进程的环境变量。
底层系统调用:execve 是唯一的系统调用,其他函数均是对它的封装。
exec函数的特点:
执行成功后不返回
若 exec
调用成功,当前进程的代码、数据、堆、栈等会被新程序完全替换,进程从新程序的入口点开始执行,不会返回原程序。(也没有办法返回)仅当 exec
调用失败时,才会返回 -1
,并继续执行原程序后续代码。
进程 ID 不变
替换前后进程的PID保持不变,因为替换的是进程的 “内容”,而非进程本身。
文件描述符继承
原进程打开的文件描述符在EXEC后默认保持打开状态。
环境变量继承
原进程所拥有的环境变量在EXEC后默认保持相同。
不是说进程替换是将新的程序所带有的代码数据,替换掉原先的代码数据吗?PID保持不变,其他全部都被替换掉了,但是!实际上只有内存管理部分大换血,而环境变量,文件描述符包括IO部分完全相同,或者说原封不动地继承了下去,两个部分解耦合。
void test1()
{//单个进程进行进程替换printf("this is a begin:pid:%d,ppid:%d\n",getpid(),getppid());execl("/usr/bin/ls","ls","-a","-l",NULL); printf("this is a end:pid:%d,ppid:%d\n",getpid(),getppid());//可以看到进程替换后代码和数据进行了替换,pid不变,execl后面的代码不再执行
}extern char** environ;void test2()
{//对exec系统调用函数进行使用char* const arr[]={"ls","-a","-l",NULL };//execlp("testcpp","testcpp",NULL);//execv("/usr/bin/ls",arr);execle("./testcpp","testcpp",NULL,environ);
}
3.环境变量
抱歉抱歉,环境变量姗姗来迟,前面已经提到了它,现在来补充认识一下吧:
环境变量是操作系统中存储的一系列键值对,用于控制系统和应用程序的行为。它们在进程的上下文中生效,影响进程的运行环境...
环境变量本质:
存储形式:以 NAME=VALUE
的格式存储,例如PATH=/home/usr/testfile。
进程关联:每个进程启动时会继承其父进程的环境变量,并可修改自身的环境变量(但通常不影响父进程)。
存储位置:进程的环境变量存储在内存中的环境变量表(由指针environ指向)(使用时记得extern char** environ),可通过main函数的第三个参数envp访问。
常见环境变量:
变量名 | 作用描述 | 典型值 |
---|---|---|
PATH | 命令搜索路径,多个路径用冒号 : 分隔 | /usr/local/sbin:/usr/local/bin:... |
HOME | 当前用户主目录 | /home/username |
USER | 当前用户名 | username |
SHELL | 用户默认 Shell 路径 | /bin/bash |
LANG | 系统语言和区域设置 | en_US.UTF-8 |
PWD | 当前工作目录(由 Shell 动态更新) | /home/username/projects |
TERM | 终端类型(影响命令行显示效果,如颜色、光标控制) | xterm-256color |
EDITOR | 默认文本编辑器 | vim 或 nano |
JAVA_HOME | Java 运行环境路径(供 Java 程序查找 JRE/JDK) | /usr/lib/jvm/java-17-openjdk |
PATH (特殊) | 注意:部分程序(如 systemd )使用 PATH 以外的变量(如 PATH 需显式设置) | - |
查看环境变量:
set//可以查看所有环境变量,包括本地变量
env//查看环境变量
查看单个环境变量:
echo $PATH
echo $USER
....
设置环境变量:
临时设置(仅当前 Shell 会话有效):
export NAME=VALUE
export MY_VAR="hello world" # 声明为环境变量(可被子进程继承)
MY_VAR="hello" # 仅为当前 Shell 的局部变量(不被子进程继承)可以用set查到,echo打印
#因为echo是内建命令,不会将环境变量继承给子进程#
删除环境变量:
unset TEST
#将TEST环境变量永久删除#
环境变量与进程的关系
-
继承性:子进程会自动继承父进程的环境变量,但父进程无法感知子进程对环境变量的修改;进程替换(exec函数族)时,默认继承当前进程的环境变量,除非经过 execle/execve 显式传递新的环境变量数组。
-
作用域:全局变量:通过 export 声明或写入系统配置文件,可被所有子进程继承。局部变量:未用 export 声明的变量,仅在当前 Shell 进程内有效,不被子进程继承。