目录
从本篇开始我们进入进程控制篇,我们将学习进程创建/进程终止/进程等待/进程替换等系列专题
回顾进程地址空间:
- C/C++语言层面上全是虚拟地址空间的地址,我们是看不到物理内存的地址,虚拟地址空间会通过页表映射关系来联系虚拟地址空间的地址和物理内存地址。
- 通常fork创建了子进程,子进程和父进程的数据都是共享的。(只读层面)
- 无论是代码/数据,在子进程要对其写入的时候,发生写时拷贝。(读写层面需要独立)
- 代码/数据都是由OS从磁盘中写入物理内存的(写入------→所以也都存在读写属性)。我们前面所说代码只读,是因为我们基本不会去修改代码。
- 子进程创建的虚拟地址/页表都是拷贝的父进程的。基本一样,除了个别属性。
- 以上rwx读写可执行权限都是在页表中实现的。
- 虚拟地址空间是时代的产物/虚拟地址空间存在的意义(3点)
- 进程的独立性保证:父子进程的代码是共享的(只读),数据是以写时拷贝各自私有一份,从而保证了进程的独立性。
进程的创建
进程的创建有两种方法
- 命令行中 ./可执行程序。shell帮助创建了进程。
- 代码中使用 fork函数 创建子进程。(fork函数的本质是在OS中多了进程)
再谈进程的概念
进程 = 内核的相关管理数据结构(task_struct + mm_struct + 页表) + 代码数据
内核的相关管理数据结构(task_struct + mm_struct + 页表):对程序的先描述再组织。
代码数据:程序的实体。
进程创建的顺序
❓进程创建是先创建内核的相关管理数据结构还是代码数据
- OS先把内核的相关管理数据结构弄好了,再把代码和数据从磁盘加载到物理内存中。
- 在Linux操作系统创建进程的内核的相关管理数据结构的时候,此刻的代码和数据处于新建状态。
【1】命令行./可执行程序 创建进程
命令行中:编译之后运行即可形成进程。
#include<stdio.h>
int main()
{
printf("hello linux!\n");
return 0;
}
【2】fork创建进程
在之前我们已经学习了fork函数及其返回值。这里再细化fork创建子进程的过程。
【回顾fork创建进程】【进程概念】启动进程 | 查看进程 | 创建进程_如何开启一个进程-CSDN博客
fork函数
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
分配新的内存块和内核数据结构给子进程 task_struct
将父进程部分数据结构内容拷贝至子进程 mm_struct
子进程的内核的相关管理数据结构(task_struct + mm_struct + 页表)要继承父进程的,很多数据直接拷贝,也有一些数据需要子进程重新设置pid,ppid等/状态/优先级。
添加子进程到系统进程列表当中,之后子进程创建之后就要被调度了。
这里添加到系统进程列表中有两个含义:
- Linux系统中所有的进程是被维护再一张双链表结构中,添加到双链表当中。
- 所有进程需要调度的话需要把PCB链入到O(1)调度算法中。
子进程的代码和数据没有加载的,只能共享父进程的代码,以写时拷贝方式私有一份数据。所以一个进程的退出奔溃并不影响另外一个进程。
- 当一个进程调用fork之后,就有两个二进制代码相同的进程。而且它们都运行到相同的地方。但每个进程都将可以开始它们自己的旅程,看如下程序:
- ❓这里看到了三行输出,一行before,两行after。进程43676先打印before消息,然后它有打印after。另一个after消息有43677打印的。注意到进程43677没有打印before,为什么呢
- 所以,fork之前父进程独立执行,fork之后,父子两个执行流分别执行。注意,fork之后,谁先执行完全由调度器决定。
#include <unistd.h>
pid_t fork(void);
返回值:自进程中返回0,父进程返回子进程id,出错返回-1
//指令查看fork函数
man fork
//创建子进程,父子各自执行各自的任务代码
int main(void)
{
pid_t pid;
printf("Before: pid is %d\n", getpid());//父
if ((pid = fork()) == -1)
perror("fork()"), exit(1);
printf("After:pid is %d, fork return %d\n", getpid(), pid);//子
sleep(1);
return 0;
}
//运行结果:
//[root@localhost linux]# . / a.out
//Before : pid is 43676
//After : pid is 43676, fork return 43677
//After : pid is 43677, fork return 0
fork函数的返回值
- 子进程返回0。
- 父进程返回的是子进程的pid。
- ❓为什么fork函数返回2次。
- ❓fork函数返回值的接收值ret存在两个值。
- 这两个问题前面我们已经详细讲解过了,这里再来回顾以下☞。
- ❓为什么父进程返回的时子进程的PID,给子进程返回的是0。
❓为什么fork函数return 2次
- fork函数在return之前把创建子进程的工作已经做完了。
- 核心工作完成之后。父子进程均存在,被调度执行后面的代码(包括return语句)。
- 所以return语句是父子进程共享的,调度父子进程的时候需要执行两次。
❓fork函数返回值的接收值ret存在两个值
- fork函数创建子进程,父子进程共享代码和数据。(只读层面)
- return的本质是向ret数据中写入。
- 会发生写时拷贝。ret父子进程各自私有一份。
- ret数据(读写层面)
- 页面对应的映射关系权限从r变为rw。
- 发生写时拷贝的时,同一个虚拟地址空间有不同的物理内存数据ret。
❓为什么父进程返回的时子进程的PID,给子进程返回的是0
- 我们为了让父进程方便对子进程进行标识,进而进行管理。
1 #include<stdio.h>
2 #include <sys/types.h>
3 #include <unistd.h>
4
5 int main()
6 {
7 pid_t id=getpid();
8 pid_t parentid=getppid();
9 printf("process is running,id=%d,parentid=%d\n",id,parentid);//只有父进程
10 sleep(3);
11 pid_t ret= fork();//创建子进程
12 if(ret == -1)
13 {
14 return 1;
15 }
16 else if(ret == 0)
17 {
18 printf("I am child process!,pid=%d,ppid:%d\n",id,parentid);
19 sleep(2);
20 }
21 else//ret>0
22 {
23 printf("I am parent process!,pid=%d,ppid:%d\n",id,parentid);
24 sleep(2);
25 }
return 0;
}
写时拷贝&页表(浅拷贝)
通常,父子代码共享,父子再不写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝的方式各自一份副本。
这里包括以前我们都是只谈了数据的写时拷贝。但是我们的代码是否能写时拷贝呢❓❓
- 当然可以。(进程替换&多进程会讲)
fork创建子进程有两个作用:
- 子进程执行和父进程类似的工作
- 子进程执行和父进程完全不同的工作,一个全新的工作。
fork常规用法
- 一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求。
- 一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数。
fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
🙂感谢大家的阅读,若有错误和不足,欢迎指正。