12.OS操作系统学习:进程空间与控制
1.内存中的进程
我们思考
多进程运行
,如何更好的区分空间?
写时拷贝
机制原理是什么呢?
我们先来看一下这个学习到的图像:内存中的进程
利用fork()函数创建子进程,使子进程和父进程共同使用一个变量。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
int val = 10;
pid_t id = fork();
if(id == 0)
{
val *= 2; //刻意改变共享值
printf("我是子进程,pid:%d ppid:%d 共享值:%d 共享值地址:%p\n", getpid(), getppid(), val, &val);
exit(0);
}
waitpid(id, 0, 0);
printf("我是父进程,pid:%d ppid:%d 共享值:%d 共享值地址:%p\n", getpid(), getppid(), val, &val);
return 0;
}
现象:会发现同一个地址,出现了两个不同的value。
**解释:1.**引出逻辑地址
:面向程序的地址,从0开始编址,每一条指令的逻辑地址就是与第一条指令之间的相对偏移。对于面向程序的地址,用户无法看到真实的绝对地址(物理地址),他们是由os统一管理的。
2.真实的物理空间不是相同的
3.发生了写时拷贝机制
2.虚拟地址和物理地址的转换
虚拟空间通过notebook和MMU进行转换为真实的空间
2.1先来看MMU:
2.2再来看页表
页表
本质上就是一张表,操作系统
会为每个 进程
分配一个 页表
,该 页表
使用 物理地址
存储。当 进程
使用类似 malloc
等需要 映射代码或数据
的操作时,操作系统
会在随后马上 修改页表
以加入新的 物理内存
。当 进程
完成退出时,内核会将相关的页表项删除掉,以便分配给新的 进程
。
2.3转换
解释:
-
其实操作系统通过页表映射发现val的值是共享的,但是进程具有独立性
-
操作系统 为 保证进程独立性,当任何一方对共享数据进行写=修改的时候,操作系统会在真的物理空间上开辟一块新的内存空间.
-
先拷贝数据,然后再修改映射关系,更改页表映射,然后再让进程进行修改.
-
所以这里的显示的地址是虚拟地址,也是相同的.
2.4 写时拷贝
定义:在数据
第一次写入到某个存储位置时,首先将原有内容拷贝
出来,写到另一位置处,然后再将数据写入到存储设备中,该技术只拷贝在拷贝初始化开始之后
修改过的数据。
这里的写时拷贝是操作系统先进性数据拷贝,更改页表映射,然后再让进程进行修改.
3.为什么存在进程空间?
1.进程地址空间保证了数据的安全性
:::
每个进程都有进程地址空间,所有的进程都要通过页表映射到物理内存,如果进程直接访问物理内存,万一进程越界非法访问、非法读写时,页表就可以进行拦截,而且直接访问物理内存对于账号信息是非常不安全的,所以保证了内存数据的安全性
:::
2.地址空间的存在,可以更方便的进行进程和进程的数据代码的解耦,保证了进程独立性的特征
:::
对于进程而言,都有独立的地址空间及页表,通过页表映射到不同的物理内存上,所以一个进程数据的改变不会影响到另一个进程,保证了进程的独立性,而对于上面我们所说的父进程和子进程而言,子进程的地址空间从父进程拷贝,页表都指向同一块物理内存,但是即使此时的数据是共享的,在修改数据的时候也会发生我们所说的写时拷贝,保证了进程的独立性
:::
3.编译器也以统一的视角来进行编译代码
4.进程的控制
1.创建
1.1创建fork()
#include <unistd.h> //所需头文件
pid_t fork(void); //fork 函数
理解:
**1.**fork()函数再操作系统内部,子进程被创建的时候,进程都在ready队列中,准备被调度,fork()之后父子进程代码被共享,return会被调度两次.
**2.**父进程返回子进程的PID,给子进程返回0
**3.**pid_t 相当于typedef int
**4.**当进程过多或者用户进程数超过限制的时候,fork()会失败
1.2写时拷贝机制
2.终止
2.1查看最近一次进程运行的退出码
echo $?
进程退出后,os会释放对应的内核数据结构、代码和数据
main函数退出,表示整个程序退出
程序中的函数退出,函数运行结束
2.2退出方式
1.对于运行的进程:kill -9 PID和ctrl+c的方式
2.内部终止通过函数exit()和_exit()实现
void exit(int status);
void _exit(int status);
但是使用上其实是有区别的:
由此可见:exit()是要刷新缓冲区的
practice:
test31.cpp
现象
test32.cpp
现象
3.exit和return的区别
1.exit是函数;return是关键字
2.exit是系统调用级别的,它表示一个进程的结束;return是语言级别的,它表示调用堆栈的返回
3.exit是进程的退出;return是函数的退出
4.exit函数结束进程,删除进程使用的内存空间,并将进程的状态返回给操作系统(一般是用0表示正常终止,非0表示异常终止);return是结束函数的执行,将函数的执行信息传其他调用函数使用
5…非主函数中调用exit和return区别很明显,但是在main函数中调用区分不大,多数情况效果一样
3.等待
理论
之前在11章时就讲过当父进程没有等待并接收其退出码和退出状态的时候,OS无法释放对应的内核数据结构+代码和数据,出现僵尸进程
-
为了避免这种情况,父进程可以通过函数
等待子进程运行结束
,此时父进程还处于阻塞状态
-
子进程必须对子进程进行负责
等待函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* status);
pid_t waitpid(pid_t pid, int* status, int options);
wait:
-
wait成功返回被等待进程pid,等待时返回0,失败返回-1
-
参数:不关心子进程退出可以设置为NULL
waitpid:
关于三个参数:
1.**pid**
表示所等子进程的 **PID**
2.**status**
该参数是一个输出型参数,由操作系统填充 ,如果传递NULL,表示不关心子进程的退出状态信息。否则**,操作系统会根据该参数,将子进程的退出信息反馈给父进程**。status不能简单的当作整形来看待,可以当作位图来看待
3.**options**
为选项,比如可以选择父进程是否需要阻塞等待子进程退出,默认为0
演示:
#include<unistd.h>
#include<sys/types.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/wait.h>
int main()
{
//演示 waitpid()
pid_t id = fork(); //创建子进程
if(id == 0)
{
int time = 5;
int n = 0;
while(n < time)
{
printf("我是子进程,我已经运行了:%d秒 PID:%d PPID:%d\n", n + 1, getpid(), getppid());
sleep(1);
n++;
}
exit(244); //子进程退出
}
int status = 0; //状态
pid_t ret = waitpid(id, &status, 0); //参数3 为0,为默认选项
if(ret == -1)
{
printf("进程等待失败!进程不存在!\n");
}
else if(ret == 0)
{
printf("子进程还在运行中!\n");
}
else
{
printf("进程等待成功,子进程已被回收\n");
}
printf("我是父进程, PID:%d PPID:%d\n", getpid(), getppid());
//通过 status 判断子进程运行情况
if((status & 0x7F))
{
printf("子进程异常退出,core dump:%d 退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));
}
else
{
printf("子进程正常退出,退出码:%d\n", (status >> 8) & 0xFF);
}
return 0;
}
代码解释:
if((status & 0x7F))
{
printf("子进程异常退出,core dump:%d 退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));
}
else
{
printf("子进程正常退出,退出码:%d\n", (status >> 8) & 0xFF);
}
- 异常退出检查:
status & 0x7F
:这里使用按位与操作检查status
的低 7 位。如果结果不为零,表示子进程是由于接收到信号而退出的,而不是正常退出的。
可以使用(**WIFEXITED(status)**
**判断进程退出情况,当宏为真时,表示进程正常退出)**代替
(status >> 8) & 0xFF
:这一部分提取了status
的高 8 位,表示子进程的退出码
可以使用(**WEXITSTATUS(status)**
相当于 **(status >> 8) & 0xFF**
**,直接获取退出码)**代替
现象:
等待时执行
options参数改为WNOHANG,也就是父进程处于非阻塞状态,父进程进入等待轮询状态,不断获取子进程是否退出,如果没有退出,就可以做别的事情。
waitpid(id, &status, WNOHANG);
pracitice:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h> //进程等待相关函数头文件
#include<stdlib.h>
int main()
{
//演示 waitpid()
pid_t id = fork(); //创建子进程
if(id == 0)
{
int time = 9;
int n = 0;
while(n < time)
{
printf("我是子进程,我已经运行了:%d秒 PID:%d PPID:%d\n", n + 1, getpid(), getppid());
sleep(1);
n++;
}
exit(244); //子进程退出
}
int status = 0; //状态
pid_t ret = 0;
while(1)
{
ret = waitpid(id, &status, WNOHANG); //参数3 设置为非阻塞状态
if(ret == -1)
{
printf("进程等待失败!进程不存在!\n");
break;
}
else if(ret == 0)
{
printf("子进程还在运行中!\n");
printf("我可以干一些其他任务\n");
sleep(3);
}
else
{
printf("进程等待成功,子进程已被回收\n");
//通过 status 判断子进程运行情况
if(WIFEXITED(status))
{
printf("子进程正常退出,退出码:%d\n", WEXITSTATUS(status));
break;
}
else
{
printf("子进程异常退出,code dump:%d 退出信号:%d\n", (status >> 7) & 1, (status & 0x7F));
break;
}
}
}
return 0;
}
现象:
4.进程替换
4.1 为什么要进行进程替换?
4.2 七大替换函数
1.excel 使用list方式
#include <unistd.h>
int execl(const char* path, const char* arg, ...);
#include <stdio.h>
#include <unistd.h>
int main()
{
//execl 函数
printf("程序替换前,you can see me\n");
int ret = execl("/usr/bin/ls", "ls", "-a", "-l", NULL);
//程序替换多发生于子进程,也可以通过子进程的退出码来判断是否替换成功
if(ret == -1)
printf("程序替换失败!\n");
printf("程序替换后,you can see me again?\n");
return 0;
}
链式传递方式:
现象:
2.execv 使用vector方式
#include <stdio.h>
#include <stdlib.h> //exit 函数头文件
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
//execv 函数
pid_t id = fork();
if(id == 0)
{
printf("子进程创建成功 PID:%d PPID:%d\n", getpid(), getppid());
char* const argv[] =
{
"ls",
"-a",
"-l",
NULL
}; //argv 表,实际为指针数组
execv("/usr/bin/ls", argv);
printf("程序替换失败\n");
exit(255); //如果子进程有此退出码,说明替换失败
}
int status = 0;
waitpid(id, &status, 0); //父进程阻塞等待
if(WEXITSTATUS(status) != 255)
{
printf("子进程替换成功,程序正常运行 exit_code:%d\n", WEXITSTATUS(status));
}
else
{
printf("子进程替换失败,异常终止 exit_code:%d\n", WEXITSTATUS(status));
}
return 0;
}
现象:
解释:WEXITSTATUS(status) != 255表示退出码如果不等于255
就代表程序替换成功
3.execlp 可以自动到PATH
变量中搜索
#include <stdio.h>
#include <stdlib.h> //exit 函数头文件
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
//execlp 函数
pid_t id = fork();
if(id == 0)
{
printf("you can see me\n");
execlp("ls", "ls", "-a", "-l", NULL); //程序替换
printf("you can see me again?");
exit(-1);
}
int status = 0;
waitpid(id, &status, 0); //等待阻塞
if(WEXITSTATUS(status) != 255)
printf("子进程替换成功 exit_code:%d\n", WEXITSTATUS(status));
else
printf("子进程替换失败 exit_code:%d\n", WEXITSTATUS(status));
return 0;
}
现象:
4.execvp 可以自动到PATH
变量中搜索
#include <stdio.h>
#include <stdlib.h> //exit 函数头文件
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
//execvp 函数
pid_t id = fork();
if(id == 0)
{
printf("子进程创建成功 PID:%d PPID:%d\n", getpid(), getppid());
char* const argv[] =
{
"ls",
"-a",
"-l",
NULL
};
execvp("ls", argv);
printf("程序替换失败\n");
exit(-1); //如果子进程有此退出码,说明替换失败
}
int status = 0;
waitpid(id, &status, 0); //父进程阻塞等待
if(WEXITSTATUS(status) != 255)
{
printf("子进程替换成功,程序正常运行 exit_code:%d\n", WEXITSTATUS(status));
}
else
{
printf("子进程替换失败,异常终止 exit_code:%d\n", WEXITSTATUS(status));
}
return 0;
}
5.execle 将自定义或当前程序中的环境变量表传给代替换程序
程序1:execle1.cpp
#include<stdio.h>
#include<stdlib.h>
#include<iostream>
using namespace std;
extern char** environ; //声明环境变量表nmain(int argc,char *argv[])
int main(int argc,char* argv[])
{
int pos=0;
while(environ[pos])
{
cout << environ[pos++] << endl;
}
for(int i=0;argv[i];i++)
{
printf("argv[%d]:%s\n",i,argv[i]);
}
printf("PATH:%s\n",getenv("PATH"));
printf("PWD:%s\n",getenv("PWD"));
printf("传递测试程序\n");
return 0;
}
程序2:execle2.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<assert.h>
extern char** environ;
int main()
{
printf("process is running...\n");
pid_t id =fork();
assert(id!=-1);
if(id==0)
{
sleep(1);
putenv("myval=100");
execle("./execle1","execle1","-a","-b",NULL,environ);
}
pid_t rid = waitpid(id,NULL,0);
if(rid>0)
{
printf("wait sucess\n");
}
return 0;
}
实验现象:
红框:printf
黄框:存储程序的命令行参数
灰框:环境变量
6.execvpe和execvp但是最后一个参数可以传递环境变量表
#include <unistd.h>
int execvpe(const char* file, char* const argv[], char* const envp[]);
7.execve 真正的程序替换函数
#include <unistd.h>
int execve(const char* filename, char* const argv[], char* const envp[]);
参数1:待替换程序的路径
参数2:待替换程序名及其参数组成的 **argv**
表
参数3:传递给待替换程序的环境变量表
practice:
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<sys/types.h>
int main(int argc,char* argv[],char* envp[])
{
pid_t id =fork();
if(id==0)
{
printf("You can see me\n");
execve("/usr/bin/ls",argv,envp);
printf("You can see me again?");
exit(-1);
}
int status=0;
waitpid(id,&status,0);
if(WEXITSTATUS(status)!=255)
printf("子进程替换成功 exit_code:%d\n", WEXITSTATUS(status));
else
printf("子进程替换失败 exit_code:%d\n", WEXITSTATUS(status));
return 0;
}
4.3 程序替换以后是否还为同一个进程
实验:
-
practice
#include
#include <unistd.h>using namespace std;
int main()
{
int n=4;
while(n)
{
cout << “程序替换成功”;
cout << " PID:" << getpid() << " PPID:" << getppid() << endl;
sleep(1);
n–;
}return 0;
}
2.practice1
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
int main(int argc,char* argv[],char* envp[])
{
pid_t id=fork();
if(id==0)
{
printf("111");
printf("process sucess PID:%d,PPID:%d",getpid(),getppid());
fflush(stdout);
execve("./practice",argv,envp);
exit(-1);
}
int status=0;
waitpid(id,&status,0);
if(WEXITSTATUS(status)!=255)
printf("子进程替换成功 exit_code:%d\n", WEXITSTATUS(status));
else
printf("子进程替换失败 exit_code:%d\n", WEXITSTATUS(status));
return 0;
}
说明:
1.程序替换不是进程替换
2.因为是同一个进程,所以对父进程没有任何影响,体现了进程间的独立性。