Linux进程概念详解:从PCB结构到fork()双返回值机制

140 阅读8分钟

1. 什么是进程

在有的教材中会看到:运行起来的程序,或加载到内存的程序叫做进程。但实际上这是不够严谨的说法。

通过上一篇操作系统文章了解到:根据冯诺依曼体系结构决定,程序执行前需先加载到内存中。并且通过日常使用,会有多个程序同时打开的情况,所以可以得到一个结论:系统当中会同时存在大量的进程。 既然会同时存在大量的进程,必然是需要操作系统来管理这些进程的。那么如何管理?这就是上篇文章提到的:先描述,再组织。

在内核层面上,管理进程只把程序加载到内存是不够的。打个比方:一个人到学校门口要求进学校,但被保安拦住,这个人说是这所学校的学生,保安需要证明,这个人说我在这个学校里,保安不同意并且说:“我也在这个学校里,难道我也是学生吗?”显然如果要证明自己学生的身份,需要的是在学校系统里的姓名、学号等一系列的信息,也就是描述这个人的学生信息。

把一个可执行程序加载到内存中这不叫做一个进程,因为操作系统管理一个进程必须要做到:先描述,再组织。 为了描述一个进程,必须要有一个进程控制块:PCB(Process Control Block) 结构,在Linux中,这个结构叫做:struct task_struct{}。虽然目前我们不懂这个PCB,但至少知道,在这个结构体中应该包含着进程相关的所有属性信息。

每当一个程序加载到内存时,操作系统要为当前程序创建一个PCB结构,并且在这个结构中,一定能让我们找到当前程序的代码和数据,这就是先描述,而且结构中还会存在一个*next指针,来将所有的进程组织起来,这样就会得到一个进程链表。所以操作系统对进程的管理并不是对代码和数据的管理,而是转化成对链表的增删查改。

所以得出一个结论:进程 = PCB(内核数据结构) + 代码和数据

image.pngLinux2.6.18源码中的struct task_struct(部分)

而所谓的进程排队,也并不是代码和数据在CPU中排队,而是存在一个调度队列struct runqueue{},通过这个调度队列来链接进程链表中的节点,通过这个节点找到对应的代码和数据喂给CPU运行。所以进程排队本质上就是让PCB进行排队。

这里再打个比方:当我们找工作时需要投递简历,简历上包含了我们的所有信息,那么在找工作的时候是我们在排队吗?其实本质上是我们的简历在排队。而这里的代码和数据就是找工作的我们,简历就是PCB。

2. task_struct

在一个task_struct结构中会包含以下信息(具体详细信息在后面介绍):

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

3. 查看进程

要查看当前进程的标识符可以通过getpid()函数:

image.png 通过man getpid指令来查看文档

接下来写一个简单的代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    pid_t id = getpid();
    while(1)
    {
        printf("I am a process, pid:%d\n", id);
        sleep(1);
    }
    return 0;
}

运行后就可以看到当前进程的pid:

image.png

那么该如何通过pid查看当前的进程呢?

3.1 方法一:通过/proc目录

在Linux根目录下有一个/proc目录。

image.png

进入目录内部,此时的2748480目录就是刚才运行的进程。

image.png

终止进程后,再次运行,pid也随之改变,/proc目录下所对应的目录名字也随之更改。

image.png image.png

进入到对应的pid:2798530目录后可以看到当前进程属性,红色框表示进程自己对应的二进制可执行文件,绿色框则表示了当前进程的工作路径,所以进程启动的时候,默认工作路径就是自己可执行程序所处的路径。

image.png

如果要更改当前路径,可以通过chdir()函数。

image.png

通过/proc目录来查看进程的方法实际上并不常用。

3.2 方法二:命令行

ps axj | head -1 ; ps axj | grep "进程名字或pid" 

在下图可以看到当前进程的pid,但还有一个ppid,这就是当前进程的父进程的标识符。

image.png

在Linux系统中,增多进程是通过父进程创建子进程的方式,来让Linux系统中的进程变多的。

要查看当前进程的父进程标识符可以通过getppid()函数。

image.png 通过man getppid指令来查看文档

通过代码来观察一下:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    pid_t ppid = getppid();    // 获取当前进程的父进程pid
    pid_t id = getpid();    // 获取当前进程pid
    while(1)
    {
        printf("I am a process, pid:%d, 我的父进程ppid:%d\n", id, ppid);
        sleep(1);
    }
    return 0;
}

image.png

但当我们多运行几次后会发现,进程本身的pid是每次都在变化的,但是父进程ppid却是不变的。

image.png

这时再通过命令行来观察一下这个pid为2815421的进程是什么。

image.png

所以我们在命令行中,启动命令/程序的时候,都会变成进程,而该进程的父进程就是bash

在Linux中,Bash (Bourne-Again SHell) 是最常用的命令行解释器(Shell),也是大多数Linux发行版的默认Shell。它的核心功能是解析用户输入的命令,与操作系统内核交互,执行脚本,并管理任务!

4. fork()函数初识

image.png 通过man fork指令来查看文档

接下来写一段代码:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    printf("我是一个进程, pid:%d, ppid:%d\n", getpid(), getppid());
    fork();
    sleep(1);
    printf("你能看到我这条消息吗?\n");
    sleep(1);
    return 0;
}

可以看到打印出了两条信息,这是因为调用fork函数后会创建一个新的子进程,也就是两个执行流,所以会出现这种情况。

image.png

接下来对代码稍微修改一下:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    printf("我是一个进程, pid:%d, ppid:%d\n", getpid(), getppid());
    fork();
    sleep(1);
    printf("你能看到我这条消息吗? pid:%d, ppid:%d\n", getpid(), getppid()\n");
    sleep(1);
    return 0;
}

这次可以看到当前进程的pid为:2865676;而fork之后创建的子进程pid为:2865677,父进程ppid为:2865676

image.png

再来看一下文档中fork的返回值:

  1. 调用后子进程的pid返回给父进程
  2. 返回0给子进程
  3. 失败返回-1,不创建子进程。

也就是fork成功后会有两个返回值。

image.png

下面通过返回值来观察:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    printf("我是一个进程, pid:%d, ppid:%d\n", getpid(), getppid());
    pid_t id = fork();
    sleep(1);
    printf("你能看到我这条消息吗? pid:%d, ppid:%d, ret = %d\n", getpid(), getppid(), id\n");
    sleep(1);
    return 0;
}

在Linux中创建一个新的子进程,子进程的task_struct会以父进程的task_struct为模板进行初始化。并且在fork之后默认共享父进程的代码和数据。

image.png

实际应用多进程会分流处理:

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main()
{
    printf("我是一个进程, pid:%d, ppid:%d\n", getpid(), getppid());
    pid_t id = fork();
    
    if(id < 0)
    {
        perror("fork");
        return 1;
    }
    else if(id == 0)
    {
        while(1)
        {
            printf("我是子进程!pid:%d, ppid:%d, ret = %d\n", getpid(), getppid(), id);
            sleep(1);
        }
    }
    else
    {
        while(1)
        {
            printf("我是父进程!pid:%d, ppid:%d, ret = %d\n", getpid(), getppid(), id);
            sleep(2);
        }
    }
    
    return 0;
}

image.png

那为什么一个函数会有两个返回值呢?来看下面的图片:

image.png

当然还有一个问题,id这个变量为什么会出现两个不同的值?这是因为发生了写时拷贝(这个具体后面文章再讲述,需要理解程序程序地址空间)。