【Linux】深入理解进程管理与fork系统调用的实现原理
- 进程
-
- 基本概念
- 描述进程-PCB
- task_struct-PCB的一种
- [task_ struct内容分类](#task_ struct内容分类)
- 组织进程
- 查看进程
- 通过系统调用获取进程标示符
-
-
- [Fork 之后的代码共享](#Fork 之后的代码共享)
-
- [1. **代码共享**](#1. 代码共享)
- [2. **数据段不共享**](#2. 数据段不共享)
- 总结
-
🌏个人博客主页:个人主页
进程
基本概念
- 课本概念:程序的一个执行实例,正在执行的程序等
- 内核观点:担当分配系统资源(CPU时间,内存)的实体
操作系统在把程序加载到内存中的同时,为了更好的管理进程,还要为每一个进程创建一个task_struct,包含进程相关的所以属性,这样对进程的管理就相当于对数据结构的管理。
描述进程-PCB
-
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
-
课本上称之为PCB(process control block),Linux操作系统下的PCB是:
task_struct
task_struct-PCB的一种
-
在Linux中描述进程的结构体叫做
task_struct
。 -
task_struc
t是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
task_ struct内容分类
-
标示符: 描述本进程的唯一标示符,用来区别其他进程。
-
状态: 任务状态,退出代码,退出信号等。
-
优先级: 相对于其他进程的优先级。
-
程序计数器: 程序中即将被执行的下一条指令的地址。
-
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
-
上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
-
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
-
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
-
其他信息
组织进程
可以在内核源代码里找到它。所有运行在系统里的进程都以task_struct链表的形式存在内核里。
查看进程
首先,我们写一段代码
使用ps ajx指令查看进程
这里为什么查到两个带有myproc关键字的进程呢?
这是因为grep指令也是一个可执行程序,当使用这个指令的时候此时也是一个进程,当我们用这个进程查看带有myproc关键字的进程,当然就会把自己查出来了。
我们可以在加上一条指令屏蔽。
📢小知识:把一个程序运行起来,本质就是在系统当中启动了一个进程!一种是执行完就退出,比如:ls pwd 指令,一种是常驻进程,一直不退,直到用户退出。
通过系统调用获取进程标示符
如果想要查看进程的更详细的信息可以通过/proc
系统文件夹查看
如:要获取PID为1的进程信息,你需要查看/proc/1
这个文件夹
📢注意:proc文件夹里面的目录都是临时文件,当进程开始就会创建一个以这个进程的pid作为名字的文件夹,进程结束的时候就会删除这个文件夹。
例如:我们通过proc文件来查看,这个进程更详细的信息。
exe -> /home/hanbo/112/lesson9/myproc
这个exe是一个链接文件,这个链接文件指向该进程的实际可执行程序在磁盘上的路径。
cwd -> /home/hanbo/112/lesson9
cwd(current work dir):当前工作目录,这就是为什么我们在创建文件的时候会在当前目录下面创建
我想可以使用系统调用chdir
来改变当前工作目录。
运行结果如下:
📢小知识:在命令行中,执行命令/执行程序,本质是bash的进程,创建子进程,来执行我们的代码。
我们如何通过系统调用来创建进程呢?
在执行fork之后就会有两个进程,一般而言,代码是会共享的,因为代码是只读的,但是数据是各自私有一份的,这是因为进程有很强的独立性,多个进程之间,运行时,互不影响,即便是父子进程,需要保证修改父进程的数据,不会影响到子进程。
因为父进程的PCB是根据磁盘加载到内存的程序的信息创建的,但是对于子进程来说只有task_struct这个结构体,没有代码和数据,所以,子进程本质是把父进程的task_struct结构体拷贝过来,这样父子进程就指向同一份代码,然后,在对子进程进行特性化设计。
📢小知识:fork之后父子进程谁先执行不确定,调度器决定。
如何创建多个子进程,进行管理呢?
cpp
#include <iostream>
#include <vector>
#include <sys/types.h>
#include <unistd.h>
using namespace std;
const int num = 10;
void SubProcessRun()
{
while(true)
{
cout << "I am sub process, pid: " <<getpid() <<" ,ppid: " << getppid() << std::endl;
sleep(5);
}
}
int main()
{
vector<pid_t> allchild;
for(int i = 0; i<num; i++)
{
pid_t id = fork();
if(id == 0)
{
// 子进程
SubProcessRun();
}
// 这里谁执行?父进程一个人
allchild.push_back(id);
}
// 父进程
cout << "我的所有的孩子是:";
for(auto child: allchild)
{
cout << child << " ";
}
cout << endl;
sleep(10);
while(true)
{
cout << "我是父进程,pid: " <<getpid() << endl;
sleep(1);
}
return 0;
}
深度理解为什么,fork之后代码会有两份?
在执行return id
之前,创建子进程的工作肯定是已经完成的了,那么之后,代码都是父子进程共享的都会执行。
实际上,在fork函数被调用之前的代码被父进程执行,而fork函数之后的代码,则默认情况下父子进程都可以执行,所以return id
其实返回了两次,然后,我们用变量进行接收的时候,因为父子进程的数据都是独立的,这时候就拿到两个返回值了。
fork之后代码共享,指的是整个程序代码共享还是只有,fork之后的程序代码共享呢?
以下是ChatGPT生成的内容
Fork 之后的代码共享
在 fork 操作之后,父进程和子进程之间的 代码共享 指的是 整个程序代码部分 共享,而不仅仅是 fork 之后的程序代码。
1. 代码共享
- fork 后,父进程和子进程共享程序的 代码段,即存放指令集的部分。
- 无论父进程还是子进程,它们都执行相同的程序代码,尽管它们的执行路径可以不同。子进程会从
fork
返回后继续执行,或者可以通过exec
系列函数加载新程序。 - 由于操作系统使用内存映射技术,这些代码段是 只读 的,并且不会被复制,从而实现父子进程的代码共享。
2. 数据段不共享
- 除了代码段,数据段 (如堆、栈等)是 不共享 的。操作系统使用 写时复制(Copy-On-Write,COW) 技术来管理数据段:
- 初始时,父子进程共享相同的数据页。
- 一旦某个进程修改某一数据页,操作系统会为该进程分配新的内存,以避免修改影响到另一个进程。
总结
- fork 后,父进程和子进程共享的是 整个程序的代码,包括加载到内存中的指令部分。
- 对于 数据段 (堆、栈等),父子进程通过 写时复制 技术共享内存,直到某个进程修改数据才会分配新的内存页。