目录

一、基本概念

二、进程控制块 - 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 会把 被修改的数据在底层中拷贝一份,让目标进程去进行修改,此时这个过程就叫做 写时拷贝。


Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐