linux|Linux操作系统-进程控制

目录
进程地址空间
为什么要有虚拟地址空间?
重新审视fork函数
写时拷贝
fork函数失败
进程终止
进程退出场景
return 与 exit 的区别
exit 与 _exit的区别
进程等待
wait函数
waitpid
进程程序替换
环境变量
获取命令行参数
exec系列的函数

进程地址空间 进程地址空间的部分概念在之前说过
C++内存管理 && 读高质量C++/C编程指南第7章_TangguTae的博客-CSDN博客
先看如下的例子

#include#include #include int val = 100; int main() { pid_t id = fork(); //创建进程,从这开始就有两个执行流 while(true) { if(id == 0) { //子进程 std::cout<<"child# val is: "<
运行结果:
linux|Linux操作系统-进程控制
文章图片

父进程和子进程在相同的地址中的内容不一样,只能说明一个问题,两个进程实际指向的并不是同一块内存,准确来说不是物理内存。
这个地址实际上是虚拟地址。
linux|Linux操作系统-进程控制
文章图片

所以说,虽然父子进程返回的地址是一样的,但都是虚拟地址(包括平常用的指针啥的都是一样的),最开始时,没有对全局变量进行修改,父子进程实际的物理地址共用同一块空间(操作系统的优化),一旦发生修改,会新开辟一块空间(写时拷贝)。
虚拟地址空间本身是一个数据结构:
参考源码:
linux|Linux操作系统-进程控制
文章图片

linux|Linux操作系统-进程控制
文章图片

用自己的总结就好比下面
struct mm_struct { unsigned long code_start//代码起始段 unsigned long code_end unsigned long init_data_start//初始化数据 unsigned long init_data_end unsigned long uninit_data_start//未初始化数据 unsigned long uninit_data_end unsigned long heap_start//堆区 unsigned long heap_brk ……//等等 }

这里面就包含,代码段、数据段、堆、栈等等所定义的不同区域的起始位置和结束位置。

申请空间的本质是:向内存索要空间,得到物理地址,然后在特定区域申请没有被使用的虚拟地址,建立映射关系(硬件MMU负责完成),返回虚拟地址。其中物理地址用户一概不可见
为什么要有虚拟地址空间?
如果我们直接访问物理地址,隐含一些问题
1、野指针的存在,容易随意访问到别的区域,甚至修改。
2、由于内存可能会存在内存碎片,一个进程的数据存放在实际内存中的位置可能不连续的,降低了访问效率。
3、增加了异常越界的概率。
为了避免这种情况引入虚拟地址空间
linux|Linux操作系统-进程控制
文章图片

通过虚拟地址空间,可以将空间连续化处理,并且还可以保护物理内存。
一个进程如果越界访问到别人的空间,首先页表中没有这层映射关系,操作系统就会终止该操作。也表中除了映射关系以外,还有对应的权限管理,如果一个进程越界自己的数据,比如写入到自己字符创数据区域,然而在页表的映射关系中,字符串只有读权限,同样也会在操作系统层面上崩溃。
重新审视fork函数 当父进程调用fork函数后,内核需要执行
1、 重新分配内存块和内核数据结构给子进程
2、将父进程部分数据结构内容拷贝至子进程(地址空间、页表映射等等内容)
3、将子进程添加到系统进程列表中
4、fork返回,调度器开始调度
linux|Linux操作系统-进程控制
文章图片

其中默认情况下,父子进程共享代码,但是数据各自私有一份。
代码共享:所有代码共享,不过一般都是从fork之后开始执行。之所以代码能共享,因为代码不可被修改的,代码分为两份浪费空间。
数据为什么不共享?
进程之间具有独立性、修改数据会相互影响。
写时拷贝
上面一个例子说道父子进程最开始时可以看到同样的数据,一旦发生修改,各自的的内容又不相同
原因:当父进程创建子进程时(fork),代码段和数据段是只读的,且此时父进程和子进程的代码和数据是共享的。
当父进程和子进程任意一个需要写入操作时,将会发生拷贝,并将数据段变为可读可写的状态,这个过程就叫做写时拷贝。
linux|Linux操作系统-进程控制
文章图片

计算机通过这样的方式,不立即进行数据的拷贝,节约空间和时间。
fork函数失败
1、系统中有太多的进程,资源不够
2、进程的数量超出了限制

进程终止 进程退出场景
1、代码运行完毕,结果正确
2、代码运行完毕,结果不正确
3、代码异常终止(ctrl c发信号终止进程)
进程退出后会有进程退出码
1、从main函数返回(常见的return 0就是进程正常结束后所返回0,退出码也就为0)
2、调用exit函数
3、_exit函数
查看退出码的方法:echo $?
为什么main函数要return 0,而不是return其他值
原因:C++/C在函数设计中0一般代表正确,而非0错误所对应一种错误的原因。
例如
linux|Linux操作系统-进程控制
文章图片

正常退出,return 0,得到的进退出码为0.
linux|Linux操作系统-进程控制
文章图片

ctrl c异常终止程序,得到进程退出码非0.

return 与 exit 的区别
return 是终止函数,在main函数下是终止进程。
而exit是直接终止进程。
exit 与 _exit的区别
_exit直接进入内核干掉进程
exit会做一些清理工作 ,例如刷新缓冲区、关闭流等
linux|Linux操作系统-进程控制
文章图片


进程等待 【linux|Linux操作系统-进程控制】子进程先退出,父进程如果不管不顾,子进程会变成僵尸进程,从而引起系统资源的泄漏。解决办法是父进程进行等待。
wait函数
linux|Linux操作系统-进程控制
文章图片

看一下库里面是如何描述的
linux|Linux操作系统-进程控制
文章图片
wait是系统调用函数,当子进程退出时,父进程调用wait函数让系统去释放资源,并且父进程可以获取到子进程的退出状态。
int* status是输出型参数,当父进程不需要关心子进程退出的状态时,可以设为空。
返回值:当等待成功,返回子进程pid,否则返回-1
测试:
#include#include #include int main() { pid_t id = fork(); //创建进程 while(true) { if(id == 0) { //子进程 int count = 0; while(true) {if(++count == 5)//count等于5的时候结束进程 exit(0); std::cout<<"i am child ..."< 0) { //父进程 sleep(1); std::cout<<"i am father ..."<

linux|Linux操作系统-进程控制
文章图片

当父进程没有等待时,子进程变成了僵尸状态。
修改代码:
#include#include #include #include int main() { pid_t id = fork(); //创建进程 while(true) { if(id == 0) { //子进程 int count = 0; while(true) {if(++count == 5) exit(0); std::cout<<"i am child ..."< 0) { //父进程 sleep(1); std::cout<<"i am father ..."<

linux|Linux操作系统-进程控制
文章图片

wait等待是一个阻塞等待过程,父进程会在wait函数这阻塞住。
linux|Linux操作系统-进程控制
文章图片
当成功等到子进程退出后,得到的返回值为子进程的pid,通过ps axj 查看进程状态可以看到此时已经没有僵尸状态的子进程了。后续没有子进程的时候wait会出错,返回值为-1.
注意:
回收资源是操作系统进行回收的,父进程只是触发了回收的动作。

waitpid
linux|Linux操作系统-进程控制
文章图片

waitpid同样也是等待进程退出。与wait有些区别
第一个参数是进程的ID,第二个参数也是一个输出型参数status,第三个参数是waitpid的选项
进程pid的值不同时效果也不一样,看官方解释
linux|Linux操作系统-进程控制
文章图片

需要注意的是:
当pid = -1 的时候表示等待任意进程
当pid > 0 的时候表示等待指定pid的进程
options选择等待的方式, 默认设置为0表示阻塞等待,设置为WNOHONG表示非阻塞等待。
status,输出型参数,操作系统会将退出信息通过这个参数返回给父进程。设置为空可忽略此参数
status总共32位,但是只关心他的低16位
linux|Linux操作系统-进程控制
文章图片

status返回值有两种情况,一个是正常退出时,低16位的高八位就是进程退出码。当进程是被信号所终止时,低16位中的低七位就是被哪个信号所杀。
所以我们要获取进程退出的状态就可以用如下两种代码获取
pid_t ret = waitpid(id,&status,0); //阻塞等待,非阻塞等待可以把0改为WNOHONG int res = (status>>8)&0xff; //正常退出 std::cout<<"eixt code is "<

进程程序替换 环境变量
环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数。
这个参数种类很多,用env可以查看当前系统的环境变量,常见的USER,PATH,PWD,SHELL、HOME等等。
现在只关心路径PATH,当我们输入一些命令行的命令时,系统是如何工作的?
1、找到这条命令2、运行它
其实cd、pwd、ls、ps等等命令都是一个程序,但是为什么不需要指定路径就可以运行呢?或者说我们运行我们自己写的程序需要加上./xxx才能运行?
其实就是环境变量PATH的作用。
$PATH 查看所有的路径
linux|Linux操作系统-进程控制
文章图片

当我们输入ls的时候,系统默认从上面路径去找这条命令。
如果我们自己写的程序也想不加./直接运行,可以将我们的可执行程序放在这些路径下,或者添加一条路径
sudo cp xxx /usr/bin 或者 export PATH=$PATH: 当前路径
获取命令行参数
ls 这样的命令可以在后面带选项,例如 ls -l,ls -al等等
程序的main函数不一定是void形参,还是可以带很多参数的
//argc代表argv的大小,argv是一个指针数组 int main(int argc, char* argv[]) { }

测试:
#include int main(int argc, char* argv[]) { for(int i = 0; i < argc; i++) { printf("argv[%d] = %s\n",i,argv[i]); } return 0; } //注意:编译的时候用 gcc main.c -std=c99

linux|Linux操作系统-进程控制
文章图片

argc默认大小为1,argv默认第一个参数是程序名称。

exec系列的函数
之前有说过,bash进程作为命令行解释器,当输入命令时,创建子进程去执行对应的程序,其中的原理就是通过exec系列的函数将程序替换,从而让子进程不在执行父进程相同的程序,而是去执行对应的程序。
总共有六种exec开头的函数,头文件unistd.h
execl与execv
l:表示列表的形式,表示可以传递多个参数,用列表的形式
v:代表数组的形式
int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
#include #include int main() { printf("running before ...\n"); //以列表形式替换 execl("/usr/bin/ls","ls","-a","-l",NULL); //后面一定要有个NULL作为结束标志 printf("running after ...\n"); return 0; }

这段代码是将execl后面的代码全都替换了,并去执行ls -a -l命令
linux|Linux操作系统-进程控制
文章图片

效果和直接在命令行上面输入 ls -a -l效果是一样的
execv则需要定义一个指针数组,char* argv[] = {"ls","-a", "-l",NULL}。效果都是一样的

execlp、execle与execvp、execve
后面的一个字母p代表可以自动搜索环境变量,e表示自己维护环境变量,需要传递。
#include #include int main() { printf("running before ...\n"); execlp("ls","ls","-a","-l",NULL); //无需写全路径 printf("running after ...\n"); return 0; }

带p的无需写全路径,自己回去环境变量里面找。

    推荐阅读