【Linux操作系统】:进程控制

news/2024/11/8 23:09:17/

目录

一、程序地址空间

1.C/C++中的程序地址空间

2.进程地址空间

进程地址空间概念

什么是地址空间?什么是区域划分?

为啥要有地址空间?

 地址空间的补充

二、进程创建

1.fork函数

2.写时拷贝 

3.fork常规用法

4.fork调用失败的原因

三、进程终止

1.进程终止的概念

 2.进程常见退出方法

3._exit函数 

4.exit函数

四、进程等待

1.什么是进程等待

2.为啥要进程等待

3.如何进行进程等待

1.wait方法

2.waitpid方法 

 3.获取子进程status

五、进程程序替换

1.实现一个简单的进程程序替换

2.进程程序替换原理 

 3.进程程序替换的函数

4.进程程序替换函数应用场景


一、程序地址空间

1.C/C++中的程序地址空间

我们在学习C/C++中我们知道这样的空间布局图!!!

我们如何去创建和访问变量呢?

本质:起始地址 + 偏移量(其实我们的变量的类型就是偏移量)

上面的这些是内存吗?不是内存!!!

我们下面来做一个小实验!!!

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{pid_t id = fork();if(id < 0){perror("fork");return 0;}else if(id == 0)    { //childprintf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);}else{     //parentprintf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);}sleep(1);return 0;
}

我们发现,输出出来的变量值和地址是一模一样的,很好理解呀,因为子进程按照父进程为模版,父子并没有对变量进行进行任何修改。可是将代码稍加改动。 

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int g_val = 0;
int main()
{pid_t id = fork();if(id < 0){perror("fork");return 0;}else if(id == 0)    { //childg_val = 100;printf("child[%d]: %d : %p\n", getpid(), g_val, &g_val);}else{     //parentprintf("parent[%d]: %d : %p\n", getpid(), g_val, &g_val);}sleep(1);return 0;
}

我们发现,父子进程,输出地址是一致的,但是变量内容不一样! 

 我们可以得出下面的结论?

OS必须负责将 虚拟地址 转化成 物理地址 。 


2.进程地址空间

进程地址空间概念

上面的图就可以说明问题,同一个变量,地址相同,其实是虚拟地址相同,内容不同其实是被映射到了不同的物理地址。 

那么我们如何去理解 虚拟地址相同,但是物理地址不同呢?

  1. 父进程有自己的虚拟地址,也有自己的页表。
  2. 子进程在被创建的时候,父进程也会将页表拷贝给子进程。
  3. 子进程在更改数据(g_val = 200)时,会发生写时拷贝,物理地址改变了,但是虚拟地址没有改变。

什么是地址空间?什么是区域划分?

 我们在创建进程的时候不仅要有 pcb,也要管理地址空间(先描述,在组织),有一个 struct mm_struct 的结构体。

为啥要有地址空间?

 

我们如何去理解 存在虚拟地址空间,可以有效的进行进程内存的安全检查呢?

我提一个问题,我们 常量区的变量 为啥不能修改呢?

我们页表中除了有映射外,还有权限的限制,当进程要修改常量区的变量时,直接在页表就没有权限。

 地址空间的补充

  1. 每个进程都有自己的页表。

二、进程创建

1.fork函数

在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。

进程调用fork,当控制转移到内核中的fork代码后,内核做:

当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序: 

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>int main()
{printf("Before: %d\n", getpid()); pid_t id = fork();if(id < 0){perror("fork");return 0;}else if(id == 0)    { //childprintf("child: %d \n", getpid());sleep(2);}else{     //parentprintf("parent: %d \n", getpid());sleep(2);}return 0;
}

所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。 

2.写时拷贝 

通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。具体见下图:

父子进程代码共享,数据独有:当任意一方试图写入,便以写时拷贝的方式拷贝一份副本 

3.fork常规用法

  • 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
  • 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。

4.fork调用失败的原因

  1. 系统中有太多的进程。
  2. 实际用户的进程数超过了限制。

 我们写一个多进程运行的一个场景

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>#define N 10typedef void (*callback_t)();                       // 函数指针void Worker()
{int cnt = 10;while (cnt){printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt);sleep(1);cnt--;}
}void createSubPorcess(int n, callback_t cb)
{int i = 0;for (i = 0; i < n; i++){sleep(1);pid_t id = fork();if (id == 0){printf("create child process success: %d\n", i);// childcb();exit(0);}}
}//__StartCTR();int main()
{createSubPorcess(N, Worker);// 只有父进程走到这里sleep(100);return 0; 
}

三、进程终止

1.进程终止的概念

main 函数的返回值可以被父进程获取的,用来判断子进程的干活的情况 。

查看上一个进程的退出码 

echo $?

我们父进程就可以通过这两个数字来判断子进程的退出情况 。

代码异常终止,退出码就没有意义了!!!


 2.进程常见退出方法

3._exit函数 

  1. _exit函数 是系统调用函数。
  2. _exit函数 在终止进程的时候,不会自动刷新缓冲区。

4.exit函数

  1. exit函数 是库函数。
  2. exit函数 在终止进程的时候,会自动刷新缓冲区。

exit最后也会调用_exit, 但在调用_exit之前,还做了其他工作:

  1. 执行用户通过 atexit或on_exit定义的清理函数。
  2. 关闭所有打开的流,所有的缓存数据均被写入。
  3. 调用_exit。

 使用_exit系统调用函数:

使用exit系统调用函数: 

 


四、进程等待

1.什么是进程等待

通过 wait/waitpid 的方式,让父进程(一般情况)对子进程进行资源回收等待过程!!!

2.为啥要进程等待

  • 之前讲过,子进程退出,父进程如果不管不顾,就可能造成‘僵尸进程’的问题,进而造成内存泄漏。
  • 另外,进程一旦变成僵尸状态,那就刀枪不入,“杀人不眨眼”的kill -9 也无能为力,因为谁也没有办法杀死一个已经死去的进程。
  • 最后,父进程派给子进程的任务完成的如何,我们需要知道。如,子进程运行完成,结果对还是不对,或者是否正常退出。
  • 父进程通过进程等待的方式,回收子进程资源,获取子进程退出信息。

3.如何进行进程等待

1.wait方法

#include <iostream>
#include <cstdlib>
#include <cstdio>#include <sys/wait.h>
#include <unistd.h>void Worker()
{int *p = NULL;int cnt = 10;while (cnt){printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);sleep(1);}
}
const int n = 10;int main()
{pid_t id = fork();if (id == 0){// childWorker();exit(1);}else{//  fatherpid_t rid = wait(NULL);if (rid == id){printf("wait success, pid: %d, rpid: %d \n", getpid(), rid);}}return 0;
}

1、进程等待可以回收僵尸进程 

2、如果子进程没有退出,那么父进程会一直阻塞等待,直到子进程僵尸了,wait自动回收,返回了。 

看下面结果图发现当父进程调用了waitpid函数时父进程就被阻塞了,阻塞期间当子进程运行完毕父进程才执行完毕,所以只有子进程退出了父进程才会退出,那么子进程就一定不是僵尸进程。

 


2.waitpid方法 

  • 如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
  • 如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
  • 如果不存在该子进程,则立即出错返回。

#include <iostream>
#include <cstdlib>
#include <cstdio>#include <sys/wait.h>
#include <unistd.h>void Worker()
{int *p = NULL;int cnt = 10;while (cnt){printf("I am child process, pid: %d, ppid: %d, cnt: %d\n", getpid(), getppid(), cnt--);sleep(1);}
}
const int n = 10;int main()
{pid_t id = fork();if (id == 0){// childWorker();exit(1);}else{// sleep(10);//  fatherint status = 0;pid_t rid = waitpid(id, &status, 0);printf("wait after\n");if (rid == id){// 我们不能对status整体使用printf("wait success, pid: %d, rpid: %d, exit sig: %d, exit code: %d\n", getpid(), rid, status&0x7F, (status>>8)&0xFF);if (WIFEXITED(status)){printf("child process normal quit, exit code : %d\n", WEXITSTATUS(status));}else{printf("child process quit except!\n");}}}return 0;
}


 3.获取子进程status

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充。
  • 如果传递NULL,表示不关心子进程的退出状态信息。
  • 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程。
  • status不能简单的当作整形来看待,可以当作位图来看待,具体细节如下图(只研究status低16比特位)。


五、进程程序替换

添加一个环境变量

 

 

1.实现一个简单的进程程序替换

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>int main()
{printf("-------------------------------------------\n");execl("/usr/bin/ls","-l","-a",NULL);printf("-------------------------------------------\n");return 0;
}

 我们查看运行结果发现,进程程序替换后还有一个printf没有执行,为啥呢?我们下面来讲解一下进程程序替换的原理。

 


2.进程程序替换原理 

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

我们就可以解释 进程程序替换后还有一个printf没有执行,为啥呢?

 


 3.进程程序替换的函数

六种进程程序替换函数

程序替换的本质

 

1、execl 进程程序替换函数

 

2、execlp 进程程序替换函数 

 

 3、execle 进程程序替换函数 

 

4、execv 进程程序替换函数  

 

 5、execvp 进程程序替换函数  

 

  6、execve 进程程序替换函数  

 

根据上面的规律我们可以总结出如下:

 

 这些函数原型看起来很容易混,但只要掌握了规律就很好记:

  • v(vector) : 参数用数组。
  • p(path) :    有p自动搜索环境变量PATH。
  • e(env) :     表示自己维护环境变量。
  • l(list) :      表示参数采用列表。
#include <unistd.h>
int main()
{char *const argv[] = {"ps", "-ef", NULL};char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};// 带l的,表示list,表示参数采用列表execl("/bin/ps", "ps", "-ef", NULL);// 带p的,可以使用环境变量PATH,无需写全路径execlp("ps", "ps", "-ef", NULL);// 带e的,需要自己组装环境变量execle("ps", "ps", "-ef", NULL, envp);// 带v的,表示vector,表示参数采用数组execv("/bin/ps", argv);// 带p的,可以使用环境变量PATH,无需写全路径execvp("ps", argv);// 带e的,需要自己组装环境变量execve("/bin/ps", argv, envp);exit(0);
}

 

4.进程程序替换函数应用场景

例如我们执行一个python的文件 

 test.py

print("hello python")

code.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>char* const argv[] = {"ls","-l","-a",NULL
};int main()
{pid_t id = fork();if(id == 0){// childprintf("I am a child , my PID:%d\n",getpid());execl("/usr/bin/python3","python3","test.py",NULL);// execl("/usr/bin/ls","ls","-l","-a",NULL);// execlp("ls","ls","-l","-a",NULL);// execv("/usr/bin/ls",argv);//execvp("ls",argv);printf("-------------------------------------\n");exit(0);}  else {pid_t rid = waitpid(-1,NULL,0);if (rid > 0){printf("wait succes!! PID:%d\n",rid);}}return 0;
}


http://www.ppmy.cn/news/1394621.html

相关文章

收集一些PostgreSQL的题目

文章目录 1. 详述PostgreSQL的MVCC&#xff08;多版本并发控制&#xff09;机制是如何工作的&#xff0c;并解释它如何帮助处理并发事务&#xff1f;2. 在PostgreSQL中&#xff0c;一个查询是如何从用户输入转化为实际的数据返回的&#xff1f;请描述一下查询执行的生命周期。3…

无人机图像识别与分析

无人机图像识别与分析是无人机技术应用的一个重要方向&#xff0c;涉及到计算机视觉、机器学习和模式识别等多个技术领域。以下是无人机图像识别与分析的一般流程和关键技术&#xff1a; 1. 图像获取 使用无人机搭载的高清摄像头、热成像相机或其他特殊传感器&#xff0c;在不…

学习或复习电路的game推荐:nandgame(NAND与非门游戏)、Turing_Complete(图灵完备)

https://www.nandgame.com/ 免费 https://store.steampowered.com/app/1444480/Turing_Complete/ 收费&#xff0c;70元。据说可以导出 Verilog &#xff01;

自注意力机制的理解

一、自注意力要解决什么问题 循环神经网络由于信息传递的容量以及梯度消失问题&#xff0c;只能建立短距离依赖关系。为了建立长距离的依赖关系&#xff0c;可以增加网络的层数或者使用全连接网络。但是全连接网络无法处理变长的输入序列&#xff0c;另外&#xff0c;不同的输…

MAC本安装telnet

Linux运维工具-ywtool 目录 1.打开终端1.先安装brew命令2.写入环境变量4.安装telnet 1.打开终端 访达 - 应用程序(左侧) - 实用工具(右侧) - 终端 #注意:登入终端用普通用户,不要用MAC的root用户1.先安装brew命令 /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/H…

Python爬虫之requests库

1、准备工作 pip install requests 2、实例 urllib库中的urlopen方法实际上就是以GET方式请求网页&#xff0c;requests库中相应的方法就是get方法。 import requestsr requests.get(https://www.baidu.com/) print(type(r)) # <class requests.models.Response> 响…

Nomachine远程黑屏通用处理方法

Nomachine远程黑屏通用处理方法 文章目录 前言正文解决步骤 总结 前言 NoMachine是一种远程桌面软件&#xff0c;它允许用户通过互联网或局域网连接到远程计算机&#xff0c;并在本地计算机上使用远程计算机的桌面环境和应用程序。它提供了高性能的图形渲染和低延迟的响应&…

Linux cp、mv命令显示进度条

1.advcpmv 平常使用cp 拷贝大文件时&#xff0c;看不到多久可以完成&#xff0c;虽然加上-v参数也只能看到正在拷贝文件&#xff0c;那就使用以下方法实现 git clone https://github.com/jarun/advcpmv.git cd advcpmv/ bash install.shmv ./advcp /usr/local/bin/ mv ./advmv …