2. 进程的基本概念和初步使用 fork 函数

2. 进程的基本概念和初步使用 fork 函数

目录

一、基本概念

二、进程控制块 - PCB

三、通过命令行和代码来查看进程相关信息

3.1. 命令行形式

3.2. 代码形式

四、初步介绍 fork 函数

4.1. 使用 fork 函数

4.2. 解释

4.3. 返回值介绍

4.4. 解释


一、基本概念

在多道程序环境下,允许多个程序并发执行,但并发性破坏了程序原本的封闭性,使其执行过程呈现间断性,并可能导致结果不可再现。为有效描述和控制程序的并发行为,操作系统引入了进程(Process)这一核心概念,以支撑操作系统的两大基本特性:并发性与共享性

说的真高大上,但是其实就是内核数据结构对象 + 自己的代码和数据


二、进程控制块 - PCB

进程创建时,操作系统为其分配一个 PCB;该结构常驻内存,可随时被系统访问,并在进程终止时被回收。PCB 是进程存在的唯一标志,系统唯有通过它才能感知并管理进程

Linux 中 PCB 是 task_struct,他的里面相关属性:

  • 标示符: 描述本进程的唯一标示符,用来区别其他进程。
  • 状态: 任务状态,退出代码,退出信号等。
  • 优先级: 相对于其他进程的优先级。
  • 程序计数器: 程序中即将被执行的下一条指令的地址。
  • 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
  • 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
  • I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
  • 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
  • 其他信息

组织形式:

双向链表组成:


三、通过命令行和代码来查看进程相关信息

3.1. 命令行形式

/proc 中是各种进程的信息,输入 即可:

ls /proc/

我们也可以查看已经运行的进程,写一个简单的代码:

#include <iostream> #include <cstdio> #include <unistd.h> using namespace std; int main() { while (true) { sleep(1); } return 0; }

运行之后,输入

ps -aux | grep test

其中,第一条信息是 刚启动的进程,而第一条信息则是 grep test 查询语句,按照上述的命令行操作,不管有没有查询到 对应的进程,都一定有 这个 grep xxx 查询语句。

3.2. 代码形式

#include <sys/types.h> #include <unistd.h> pid_t getpid(void); pid_t getppid(void);

getpid() 获得是 当前进程的 id 信息,getppid() 获得是 父进程的id信息

#include <iostream> #include <cstdio> #include <unistd.h> #include <sys/types.h> using namespace std; int main() { while (true) { sleep(1); std::cout << "父进程id: " << getppid() << " " << "当前进程id: " << getpid() << std::endl; } return 0; }

这次我们输入这个命令:

ps aux | head -1 && ps -aux | grep test

可以清楚每个数值的含义:

我们可以发现 命令行的 pid 和 代码运行出来的 pid 值是一样的,没有问题。

当然上面的命令我们也可以使用

ps ajx | head -1 && ps -ajx | grep test

来进行,可以更好的观察到 pid 等相关信息


四、初步介绍 fork 函数

4.1. 使用 fork 函数

命令行输入:

man 2 fork
#include <sys/types.h> #include <unistd.h> pid_t fork(void);

官方介绍:

fork() creates a new process by duplicating the calling process. The new process is referred to as the child process. The calling process is referred to as the parent process.

fork通过复制调用进程来创建一个新进程。新进程被称为子进程,调用进程则被称为父进程。

先写个简单的代码:

#include <iostream> #include <cstdio> #include <unistd.h> using namespace std; int main() { cout << "父进程: pid: " << getpid() << " 开始创建子进程" << endl; pid_t id = fork(); cout << "子进程: pid: " << getpid() << " 开始执行" << endl; return 0; }

结果可能会和大家想的不一样:

为什么会打印出来两个 子进程,并且 子进程的 pid 不一样?

4.2. 解释

咱们主要是利用现有的知识,进行反推。

1. pid 是不会骗人的,它是系统分配的, 调用了系统接口,一定是正确的,通过对应的数值我们就可以看出来 打印出来的 两个“子进程” 本质上其实是,一个 父进程(pid=441397) 一个是 子进程 (pid = 441398)。

2. 是怎么造成这个结果的,我们看一下上面的官方的介绍:

fork() creates a new process by duplicating the calling process. The new process is referred to as the child process. The calling process is referred to as the parent process.

fork通过复制调用进程来创建一个新进程。新进程被称为子进程,调用进程则被称为父进程。

这个“复制” 非常有意思,怎么有意思呀?复制的本质是拷贝,在计算机中,有两种拷贝方式:深拷贝和浅拷贝,根据上述的 1 的分析,我们其实都可以知道,这个是浅拷贝。

父进程 和 子进程 公用一套 代码和数据,这就是为什么 会打印出 两个 “子进程” 的原因,子进程这个是 不可修改的,而 pid 是各自的、不同的,因此我们才会发现上述的内容。

3. 为什么会这样子设计呢?以及什么时候子进程会有属于自己专属的代码和数据呢?以后再说。

4.3. 返回值介绍

这个 返回值也大有讲究:

RETURN VALUE
On success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately.

翻译:

返回值
成功时,父进程中返回子进程的 PID,子进程中返回 0。失败时,父进程中返回 -1,此时不会创建子进程,并且会适当地设置 errno。

在写一个对应的代码:

#include <iostream> #include <cstdio> #include <unistd.h> using namespace std; int main() { cout << "父进程: pid: " << getpid() << " 开始创建子进程" << endl; pid_t id = fork(); if (id < 0) { cerr << "创建子进程失败" << endl; return 1; } else if (id == 0) { // child while (true) { sleep(1); cout << "我是子进程," << "当前 pid: " << getpid() << " , 父进程id: " << getppid() << endl; } } else { // father while (true) { sleep(1); cout << "我是父进程," << "当前 pid: " << getpid() << " , 父进程id: " << getppid() << endl; } } cout << "main函数进程 id: " << getpid() << endl; return 0; }

看完这个代码,其实 很多人 都已经云里雾里了,我们会先抛出下面几个问题,以及一些名词,不可能同时讲完,但是会引出一些观点和知识。

问题:

  • 为什么 fork 给父进程和子进程返回各自不同的返回值
  • 为什么一个函数会返回两次
  • 为什么一个变量,既等于 0 又 大于 0 ,导致 if else 同时成立?

4.4. 解释

1. 为什么 fork 给父进程和子进程返回各自不同的返回值?

换而言之,就是系统是怎么区分 子进程 和 父进程的,答案就是不同的返回值。

在父进程中,fork() 返回子进程的 PID,是大于0的,父进程需要知道 子进程 的 pid,对 子进程进行管理,例如:杀死进程,等待进程等操作,而 在子进程中,fork() 返回0,也比较好理解,fork 返回值是整数,>0 的是 子进程的 pid,< 0 表示失败,只剩下 =0,所以子进程拿到的返回值就是 0.

2. 为什么一个函数会返回两次。

这个现象和上面的很像,上面的 4.1. 中的例子,我们已经可以发现 fork() 创建的本质,其实是浅拷贝,在一个堆区中,执行了相同的打印代码。而这次的代码,而是更好的揭露里面的一个内容:fork在内核中只被调用了一次,但返回动作发生了两次(发生在两个不同的进程中)。

具体的过程就是:

  1. 父进程执行到 fork(),进入到内核中,内核开始复制父进程的 PCB、内存数据、文件描述符等,创建出一个几乎一模一样的子进程。
  2. 内核创建完子进程后,父进程子进程都会从内核退出到当前的main中,并执行“系统调用返回”指令,其中父进程返回:内核将子进程的 PID写入父进程的 id 变量所在的内存位置。子进程返回:内核将 0 写入子进程的 id 变量所在的内存位置,其中,父子进程的 id 变量位于各自的独立内存空间,互不干扰)。

3. 为什么一个变量,既等于 0 又 大于 0 ,导致 if else 同时成立?

一个变量:变量的地址指向与一个物理地址中,此地址中存储具体的二进制。已知:同一个物理地址中,是无法等于两个数值的,只能满足一个 if else 条件,因此我们可以得出一个结论:

这一定是两个物理地址,并且父进程一个物理地址,子进程另一个物理地址,只不过他们的变量名字都是 fork() 的返回值:id!!!

我先用代码来证明这一点:

#include <iostream> #include <cstdio> #include <unistd.h> using namespace std; int gval = 100; int main() { printf("父进程开始执行,pid:%d\n", getpid()); pid_t id = fork(); if (id < 0) { perror("fork failure"); return 1; } else if (id == 0) { // child printf("我是一个子进程!我的pid: %d, 我的父进程id: %d,gval: %d\n", getpid(), getppid(), gval); sleep(5); while (1) { sleep(1); printf("子进程修改变量: %d->%d\n", gval, gval + 10); gval += 10; printf("我是一个子进程!我的pid: %d, 我的父进程id: %d\n", getpid(), getppid()); } } else { // father while (1) { sleep(1); printf("我是一个父进程!我的pid: %d, 我的父进程id: %d,gval: %d\n", getpid(), getppid(), gval); } } printf("进程开始执行,pid:%d\n", getpid()); return 0; }

我们可以发现,这个 gval 全局变量,在不同进程的中,值是可以被任意修改的,且不同进程不一样,这个是怎么回事,说明:每个进程都有他独有的代码和数据,也就是此时 拷贝由原来的浅拷贝改变为了深拷贝!!!

我们知道代码是不变的, 数据是可以发生变化的,其中 父子进程中,有一个数据发生了改变,那么此时,OS 会把 被修改的数据在底层中拷贝一份,让目标进程去进行修改,此时这个过程就叫做写时拷贝。