Linux进程概念(上)

本文涉及的产品
网络型负载均衡 NLB,每月750个小时 15LCU
公网NAT网关,每月750个小时 15CU
应用型负载均衡 ALB,每月750个小时 15LCU
简介: 冯·诺依曼体系结构概述,包括存储程序概念,程序控制及五大组件(运算器、控制器、存储器、输入设备、输出设备)。程序和数据混合存储,通过内存执行指令。现代计算机以此为基础,但面临速度瓶颈问题,如缓存层次结构解决内存访问速度问题。操作系统作为核心管理软件,负责资源分配,包括进程、内存、文件和驱动管理。进程是程序执行实例,拥有进程控制块(PCB),如Linux中的task_struct。创建和管理进程涉及系统调用,如fork()用于创建新进程。

1. 前言,冯诺依曼体系结构

冯·诺依曼体系结构是计算机领域的一种设计理念,由美籍匈牙利科学家约翰·冯·诺依曼提出。
该体系结构的主要特点包括:

  1. 存储程序:程序和数据以二进制形式存储在计算机的存储器中。
  2. 程序控制:计算机按照程序中指令的顺序依次执行操作。
  3. 五大组件
    • 运算器:负责执行算术和逻辑运算。
    • 控制器:控制计算机的操作,协调各组件的工作。
    • 存储器:存储数据和程序。
    • 输入设备:用于将数据输入到计算机中。
    • 输出设备:将处理结果输出。

da3ac95c75521f36bb64254e7be51e32

关于冯诺依曼体系结构,必须注意以下几点:

  • 这里的存储器指的是内存。
  • 不考虑缓存情况,这里的CPU能且只能对内存进行读写,不能访问外设(输入或输出设备),因为外设的速度太慢,根据短板效应,会导致整机的效率太低!
  • 外设(输入或输出设备)要输入或者输出数据,也只能写入内存或者从内存中读取。
  • 一句话,所有设备都只能直接和内存打交道。

计算机里几乎所有的设备都有数据存储的能力!
对于CPU这个设备,它的处理速度是最快的,紧接着是内存,然后是各种外设(磁盘)。

由此就能得到一个存储分级图:
image-20240622234504853
以CPU为中心,距离CPU越近,存储效率越高,但是相应的造价也就越贵!

  • 如果我们一味的只追求速度,不断地扩大寄存器和Cache级别存储的容量是可以做到的吗?答案是肯定的,但是这样所带来的结果一定是这台计算机将会非常的昂贵,哪怕是我们的超级计算机也没有采用这种方案。
  • 如果我们一味的只追求价格,全部都用便宜的存储介质,虽然价格很便宜,但是这样带来后果就是计算基本用不了。

当代的普通计算机,兼顾了价格和性能,基本做到了家家都能用的起计算机。

冯·诺依曼体系结构的优点包括:

  1. 通用性强:适用于多种不同的应用和任务。
  2. 结构简单:易于理解和实现。
  3. 可扩展性好:能够通过增加硬件和软件来扩展计算机的功能。

冯·诺依曼体系结构对现代计算机的发展产生了深远影响,大多数计算机都采用了这种体系结构。然而,随着技术的不断进步,也出现了一些对冯·诺依曼体系结构的改进和扩展。

2. 操作系统

2.1 概念

操作系统是一款进行软硬件资源管理的软件!同时也是计算机第一个加载的软件。

任何计算机系统都包含一个基本的程序集合,称为操作系统(OS)。笼统的理解,操作系统包括:

  • 内核(进程管理,内存管理,文件管理,驱动管理)
  • 其他程序(例如函数库,shell程序等等)

那么为什么要有操作系统呢?

操作系统是通过将软硬件资源管理好(手段),给用户提供良好(易用、稳定、高效、安全)的使用环境的(目的)。

2.2 如何理解“管理”

在整个计算机软硬件架构中,操作系统的定位是:一款纯正的搞“管理”的软件。
操作系统管理的核心是:

  1. 进程管理;
  2. 内存管理;
  3. 文件/IO管理;
  4. 驱动管理。

既然操作系统是计算机系统的管理者,那么操作系统是如何做到管理这些软硬件的呢?
首先,答案是这六个字:

==先描述,再组织==。

任何管理工作都可以用这六个字进行计算机建模!就拿我们在C/C++的小项目来说,在写项目之初,我们总是先写struct/class。好比我们写学生信息管理项目,我们一定会先创建一个student类或者student结构体,来存储学生的各类信息,这就是先描述。随后根据这些类和结构体进行详细的方法实现,这就是再组织。

2.3 系统调用和库函数概念

9f36fb3899b82550257d65c6afa2f0bf

由这个操作系统层状图可以看出,用户在最上层,一般一个用户想要访问最底层的OS数据或者访问硬件,那么就必须要贯穿整个层状结构!但是操作系统对我们是“不信任”状态,怕群众中有坏人,直接破坏了一些操作系统的底层,所以用户必定会调用系统调用!
image-20240622235553491

但是,系统调用使用起来会比较麻烦

  • 站在使用系统的人,使用诸如shell、图形化界面这一类的外壳程序,通过这些外壳程序进行系统调用,就会方便很多。
  • 站在系统开发人员的角度,操作系统对外会表现为一个整体,但是会暴露自己的部分接口,供上层开发使用,这部分由操作系统提供的接口,叫做系统调用。系统调用在使用上,功能比较基础,对用户的要求相对也比较高,所以,有心的开发者可以对部分系统调用进行适度封装,从而打包形成库,专业人士做专业的事,这些库交给他们去维护,有了库,对于开发者就很有利于更上层用户或者开发者进行二次开发,从而提高了开发效率并且降低了开发了成本。

3. 进程

3.1 进程的基本概念

课本概念:

程序的一个执行实例,正在执行的程序等

内核观点:

担当分配系统资源(CPU时间,内存)的实体。

操作系统中可能会同时存在非常多的“进程”,那么操作系统是如何管理如此之多的进程呢?答案依旧和我们上面所提到的方式一样:先描述,再组织!

3.2 描述进程——PCB

Linux是用C语言写的,所以描述进程就需要用到struct结构体,结构体里包含了进程的各种属性,这个结构体也就叫做进程控制块

  • 进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。
  • 课本上称之为PCB(process control block),而Linux操作系统下的PCB具体是是: task_struct。

把PCB抽象成struct结构体代码:

struct PCB
{
    // id
    // 代码地址&&数据地址
    // 状态
    // 优先级
    // 链接字段
    struct PCB *next; // 下一个PCB的地址
};

此时,回答一个问题,什么是进程?

  • 进程就是把磁盘中的程序加载拷贝到内存中去。
  • 进程 = 可执行程序 + 内核数据结构(PCB)
    PCB就是为了方便OS对进程进行管理。

3.2.1 task_struct

什么是task_struct?

  • task_struct是PCB的一种。
  • 在Linux中描述进程的结构体叫做task_struct。
  • task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。

3.2.2 task_ struct内容分类

1. 标识符(pid or process id):描述本进程的唯一标识符,用来区别其他进程。
查看pid的命令:

ps options

进程状态查看:

ps aux / ps ajx command(这三个字母的顺序可以任意颠倒)

例如,使用命令查看可执行程序的进程状态:

ps ajx | grep test

5f938c6d5d02bb7857c71cacf722f923
这样看起来好干,不知道每一列的数据对应的都是什么意思,于是我们想查看列属性:

ps ajx | head -1

image-20240622235724459
进程的第一行信息就显示出来了。
拼接一下命令:
ps ajx | head -1 && ps ajx | grep test

于是就可以得到这样的结果
760b761357f42d227276c29866cebe9f
此时,终止右边的进程,再次查询时会发现:
image-20240622235824716
./test不见了,也就是我们的可执行程序执行结束了,也就不再属于进程了。
由此可以得出一个结论,我们所有的指令、软件包括自己写的程序等,只要运行起来了,最终都是进程!

那么如何使用代码获取自己的pid呢?来看一下man手册里的部分描述:
image-20240622235832810
用一段代码举个栗子:

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

int main()
{
   
   
    int count = 100;

    while (count--)
    {
   
   
       pid_t id = getpid();
       printf("%d, 我的pid:%d\n", count, id);                                              
       fflush(stdout);
       sleep(1);
    }
    printf("\n");

    return 0;
 }

运行一下:
image-20240622235844728

这时就能看到该可执行程序的pid了,同时我们也证明了这个pid准确无误。

如果想要结束一个进程呢?命令如下

kill -9 pid(杀死pid为xxx的进程)

通过上面举的一系列的栗子,不难发现进程每次运行所分配的pid都不一样,但是又能发现PPID都是,这是为什么呢,这个PPID又是什么呢?

进程还有自己的父进程id,这个PPID就是父进程ID,在命令行中,父进程一般就是命令行解释器bash!

image-20240622235851779
引申一下,在Linux中创建进程的方式有:

  1. 命令行中直接启动进程,也就是手动启动。
  2. 通过代码来进行进程创建。

启动进程的本质就是创建进程,一般都是通过父进程创建的!以父进程为模板,就父进程中大部分的属性和内容赋给了子进程,所以Linux中的进程关系就注定会有一种关系,叫作父子关系。Linux中的进程派生创建是单血缘关系。

我们在命令行启动的进程,都是bash的子进程!

那么如何使用代码获取父进程的pid呢?其实和获取pid的方式基本相似,来看一下man手册里的部分描述:
image-20240622235859932
同样用一段代码举个栗子:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
   
   
   while (1)
   {
   
   
        printf("my pid is <%d>, my parent pid is <%d>", getpid(), getppid());
        sleep(1);
   }

   return 0;
}

image-20240622235906653

除了ps外,Linux中查看进程还有第二种方式:

在Linux下存在一个proc目录,它是Linux为我们维护的动态目录结构。使用命令ls /proc/进入该目录:

image-20240622235910342

验证一下:
image-20240622235914397
image-20240622235920179
以上是分别在进程运行时和把进程结束的情况所查看的目录,不难发现当进程运行时,我们能找到对应的进程目录,结束时目录也就被杀死结束了。
得到结论:

proc是一个动态的目录结构,它存放了所有存在进程的目录名称,这个目录名称就是以pid命名的。

我们来看一下这个进程目录里有什么东东:
image-20240622235928138

(这里我截取了一部分)
exe:这个进程依旧能找到我们的可执行程序具体在哪个目录下(看来没有忘本o(╥﹏╥)o)。

cwd:全称current work directory也叫当前工作目录,当前进程记录了该进程的当前工作目录。默认情况下,进程启动所处的路径就是当前路径!

如何更改当前工作目录呢?使用函数chdir(),如下chdir的用法:
image-20240622235946303

2. 状态: 任务状态,退出代码,退出信号等。

3. 优先级: 相对于其他进程的优先级。

4. 程序计数器: 程序中即将被执行的下一条指令的地址。

5. 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。

6. 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。

7. I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。

8. 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。

9. 其他信息

3.3 通过系统调用创建进程——fork初识

如何理解启动进程这种行为呢?

本质其实就是系统多了一个进程,OS要管理的进程也就多了一个,进程 = 可执行程序 + task_struct对象(内核对象),创建一个进程就是要申请内存,保存当前进程的可执行程序 + task_struct对象,并将task_struct对象添加到进程列表中!

3.3.1 fork使用

首先运行一下 man fork 来认识fork
(好长好长,截取一部分看看吧~)
image-20240623000052194
调用一下fork()

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

int main()
{
   
   
    printf("I am a parent process,my pid is <%d>\n", getpid());
    int ret = fork();
    while (1)
    {
   
   
        printf("I am a process,my pid is <%d>,ppid is <%d>,ret=%d\n", getpid(), getppid(), ret);
        sleep(1);
    }
    return 0;
}

观察现象:
image-20240623000104551
image-20240623000110716

我们发现,fork()函数有两个返回值,其中一个返回值返回的是子进程的pid,另一个返回值是0。结合目前能得出结论,只有父进程执行fork()之前的代码(此时只有父进程没有子进程),fork()之后,父子进程都要执行后续的代码。

震惊!!一个函数居然会有两个返回值?!

fork代码的一般写法:

  1. 我们为什么要创建子进程?
    因为我们想要子进程协助父进程完成一些工作,有些工作是单进程解决不了的!
  2. 我们创建子进程就是为了让子进程和父进程做不一样的事情的,那么如何保证他们是做不一样的事的呢?
    可以通过判断fork的返回值,判断是谁父进程谁是子进程,然后让他们执行不同的代码片段!

那么fork在现实中到底应该怎么使用呢?这里使用代码模拟一下类似于迅雷的边下边播功能:

#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
   
   
    printf("I am a parent process,my pid is <%d>\n", getpid());
    pid_t ret = fork();

    // 进程创建失败
    if (ret < 0)
    {
   
   
        perror("fork");
        return 1;    
    }    
    else if (ret == 0)    
    {
   
       
        // child    
        while (1)    
        {
   
                                                                                                                                                                  
            printf("I am a child process, my pid is <%d>, ppid is <%d>, ret=%d, loading...\n", getpid(), getppid(), ret);    
            sleep(1);
        }    
    }    
    else
    {
   
   
        // Parent
        while (1)
        {
   
   
            printf("I am a parent process, my pid is <%d>, ppid is <%d>, ret=%d, playing...\n", getpid(), getppid(), ret);
            sleep(1);
        }
    }
    return 0;
}

image-20240623000117712

可以说震碎三观的又来了,两个条件判断同时进行,两个while死循环同时进行,这在我们日常书写代码是根本不可能发生的事儿~

3.3.2 fork原理

1.fork干了什么事情

fork创建子进程,系统中会多一个子进程

  1. 以父进程为模板,为子进程创建PCB(模板并不是严格的拷贝,只是大部分属性相同,少部分属性不同就比如子进程的pid和ppid和父进程就不同)
  2. 我们所创建的子进程,是没有代码和数据的!!目前是和父进程共享代码和数据!所以,fork之后,父子进程会执行一样的代码。

fork之前父子进程也是都能看到所有代码的。
但是为什么子进程不从头开始执行呢?

我们日常的程序也要从上到下按顺序执行,这是因为在计算机中存在pc程序计数器/eip指针(保存当前正在执行的指令的下一个,当执行完当前指令时,pc/eip会自动往后更新),当父进程执行完fork,pc/eip指向fork后续的代码,如果从头开始执行,pc/eip也会被子进程继承,从而会导致和父进程一样再次创建子进程从而造成死循环~

2.为什么fork会有两个返回值

抽象一下fork函数
image-20240623000148008

我们知道,fork之后代码会共享,这里可以理解一下,就是fork做完核心工作的代码会共享,从上图可以看到,return也是代码也会被共享,因此父进程被调度要执行return,子进程被调度也要执行return,所以说fork就会有两个返回值。(但真是情况是,操作系统是通过一些寄存器来做到返回值返回两次的)

3.为什么fork的两个返回值,会给父进程返回子进程的pid,给子进程返回0

4.fork之后,父子进程谁先运行

fork之后创建完子进程只是一个开始,创建完子进程之后,系统的其他进程,父进程和子进程接下来要被调度执行。当父进程的PCB都被创建并在运行队列中排队的时候,哪一个进程的PCB先被选择调度,哪个进程就先运行!所以说,谁先运行是不确定的,由各自PCB中的调度信息(如时间片、优先级等)和调度算法共同决定,也就是由操作系统自主决定!

结束父进程不会影响子进程,结束子进程也不会影响父进程。进程之间运行的时候,无论进程之间是什么关系,是具有独立性的!那么系统是如何做到这种独立性的呢?

进程间的独立性,首先是表现在有各自的PCB,代码本身是只读的,所以进程之间不会相互影响。

虽然代码是共享的,但是数据父子进程是会修改的,它们会想办法将数据各自私有一份!那么又是怎么做到的呢?

写时拷贝,在最开始的创建子进程的时候,我们是以浅拷贝的方式让子进程共享代码的,一旦父子进程开始尝试进行写入时,系统会把对应的变量进行深拷贝。

5.如何理解同一个变量会有不同的值

返回的时候发生了写时拷贝,所以同一个变量会有不同的值。

相关实践学习
每个IT人都想学的“Web应用上云经典架构”实战
基于阿里云,构建一个企业web应用上云经典架构,让IT从业者体验企业级架构的实战训练。
高可用应用架构
欢迎来到“高可用应用架构”课程,本课程是“弹性计算Clouder系列认证“中的阶段四课程。本课程重点向您阐述了云服务器ECS的高可用部署方案,包含了弹性公网IP和负载均衡的概念及操作,通过本课程的学习您将了解在平时工作中,如何利用负载均衡和多台云服务器组建高可用应用架构,并通过弹性公网IP的方式对外提供稳定的互联网接入,使得您的网站更加稳定的同时可以接受更多人访问,掌握在阿里云上构建企业级大流量网站场景的方法。 学习完本课程后,您将能够: 理解高可用架构的含义并掌握基本实现方法 理解弹性公网IP的概念、功能以及应用场景 理解负载均衡的概念、功能以及应用场景 掌握网站高并发时如何处理的基本思路 完成多台Web服务器的负载均衡,从而实现高可用、高并发流量架构
目录
相关文章
|
3天前
|
人工智能 监控 Linux
【Linux】进程控制深度了解(下)
【Linux】进程控制深度了解(下)
20 6
|
3天前
|
Unix Linux
linux进程状态基本语法
linux进程状态基本语法
|
3天前
|
缓存 Linux 编译器
【Linux】多线程——线程概念|进程VS线程|线程控制(下)
【Linux】多线程——线程概念|进程VS线程|线程控制(下)
11 0
|
3天前
|
存储 Linux 调度
【Linux】多线程——线程概念|进程VS线程|线程控制(上)
【Linux】多线程——线程概念|进程VS线程|线程控制(上)
14 0
|
3天前
|
存储 NoSQL Unix
【Linux】进程信号(下)
【Linux】进程信号(下)
16 0
|
3天前
|
安全 Linux Shell
【Linux】进程信号(上)
【Linux】进程信号(上)
13 0
|
3天前
|
消息中间件 Linux
【Linux】进程间通信——system V(共享内存 | 消息队列 | 信号量)(下)
【Linux】进程间通信——system V(共享内存 | 消息队列 | 信号量)(下)
14 0
|
3天前
|
消息中间件 存储 Linux
【Linux】进程间通信——system V(共享内存 | 消息队列 | 信号量)(上)
【Linux】进程间通信——system V(共享内存 | 消息队列 | 信号量)(上)
11 0
|
3天前
|
安全 Linux 数据格式
【Linux】进程通信----管道通信(下)
【Linux】进程通信----管道通信(下)
14 0
|
3天前
|
Unix Linux
【Linux】进程通信----管道通信(上)
【Linux】进程通信----管道通信(上)
20 0